【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【17】认证服务01—短信/邮件/异常/MD5


持续学习&持续更新中...

守破离


【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【17】认证服务01

环境搭建

C:\Windows\System32\drivers\etc\hosts

java 复制代码
192.168.56.10 gulimall.com
192.168.56.10 search.gulimall.com
192.168.56.10 item.gulimall.com
192.168.56.10 auth.gulimall.com

Nginx配置:(记得使用Nginx动静分离)

# ...

http {
    # ...

    upstream gulimall {
       server 192.168.193.107:88;
    }

    include /etc/nginx/conf.d/*.conf;
}

网关:

yml 复制代码
        - id: gulimall_auth_route
          uri: lb://gulimall-auth
          predicates:
            - Host=auth.gulimall.com

gulimall-auth:

java 复制代码
@Controller
public class LoginController {
    @GetMapping("/login.html")
    public String loginPage() {
        return "login";
    }

    @GetMapping("/reg.html")
    public String regPage() {
        return "reg";
    }
}

或者:

java 复制代码
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
    /**
     * 视图映射
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {

        /**
         *     @GetMapping("/login.html")
         *     public String loginPage(){
         *          //空方法
         *         return "login";
         *     }
         */
        //只是get请求能映射
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

验证码倒计时

前端:

java 复制代码
    $(function () {
        $("#sendCode").click(function () {
            //2、倒计时
            if ($(this).hasClass("disabled")) {
                //正在倒计时。
            } else {
                //1、给指定手机号发送验证码
                // $.get("/sms/sendEmail?email=" + $("#phoneNum").val(), function (data) {
                $.get("/sms/sendcode?phone=" + $("#phoneNum").val(), function (data) {
                    if (data.code != 0) {
                        alert(data.msg);
                    }
                });
                timeoutChangeStyle();
            }
        });
    })

    var num = 60;

    function timeoutChangeStyle() {
        $("#sendCode").attr("class", "disabled");
        if (num == 0) {
            $("#sendCode").text("发送验证码");
            num = 60;
            $("#sendCode").attr("class", "");
        } else {
            var str = num + "s 后再次发送";
            $("#sendCode").text(str);
            setTimeout("timeoutChangeStyle()", 1000);
        }
        num--;
    }

短信服务

购买短信套餐后,扫码激活,然后绑定测试手机号码:

然后点击:调用API发送短信 按钮 (使用【专用】测试签名/模板)

然后 发起调用 ,复制相关信息即可

增加权限授予RAM子账号SMS和MPush的权限。

xml 复制代码
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>alibabacloud-dysmsapi20170525</artifactId>
            <version>3.0.0</version>
        </dependency>
java 复制代码
// This file is auto-generated, don't edit it. Thanks.
package com.atguigu.gulimall.auth.sms;

import com.aliyun.auth.credentials.Credential;
import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
import com.aliyun.sdk.service.dysmsapi20170525.AsyncClient;
import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsResponse;
import com.google.gson.Gson;
import darabonba.core.client.ClientOverrideConfiguration;

import java.util.concurrent.CompletableFuture;

public class SendSms {
    public static void main(String[] args) throws Exception {

        // HttpClient Configuration
        /*HttpClient httpClient = new ApacheAsyncHttpClientBuilder()
                .connectionTimeout(Duration.ofSeconds(10)) // Set the connection timeout time, the default is 10 seconds
                .responseTimeout(Duration.ofSeconds(10)) // Set the response timeout time, the default is 20 seconds
                .maxConnections(128) // Set the connection pool size
                .maxIdleTimeOut(Duration.ofSeconds(50)) // Set the connection pool timeout, the default is 30 seconds
                // Configure the proxy
                .proxy(new ProxyOptions(ProxyOptions.Type.HTTP, new InetSocketAddress("<your-proxy-hostname>", 9001))
                        .setCredentials("<your-proxy-username>", "<your-proxy-password>"))
                // If it is an https connection, you need to configure the certificate, or ignore the certificate(.ignoreSSL(true))
                .x509TrustManagers(new X509TrustManager[]{})
                .keyManagers(new KeyManager[]{})
                .ignoreSSL(false)
                .build();*/

        // Configure Credentials authentication information, including ak, secret, token
        StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
                // Please ensure that the environment variables ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET are set.
                .accessKeyId("xxxx")
                .accessKeySecret("xxxx")
                //.securityToken(System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN")) // use STS token
                .build());

        // Configure the Client
        AsyncClient client = AsyncClient.builder()
                .region("cn-shanghai") // Region ID
                //.httpClient(httpClient) // Use the configured HttpClient, otherwise use the default HttpClient (Apache HttpClient)
                .credentialsProvider(provider)
                //.serviceConfiguration(Configuration.create()) // Service-level configuration
                // Client-level configuration rewrite, can set Endpoint, Http request parameters, etc.
                .overrideConfiguration(
                        ClientOverrideConfiguration.create()
                                  // Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
                                .setEndpointOverride("dysmsapi.aliyuncs.com")
                        //.setConnectTimeout(Duration.ofSeconds(30))
                )
                .build();

        // Parameter settings for API request
        SendSmsRequest sendSmsRequest = SendSmsRequest.builder()
                .signName("阿里云短信测试")
                .templateCode("xxxx")
                .phoneNumbers("xxxx")
                .templateParam("{\"code\":\"1111\"}")
                // Request-level configuration rewrite, can set Http request parameters, etc.
                // .requestConfiguration(RequestConfiguration.create().setHttpHeaders(new HttpHeaders()))
                .build();

        // Asynchronously get the return value of the API request
        CompletableFuture<SendSmsResponse> response = client.sendSms(sendSmsRequest);
        // Synchronously get the return value of the API request
        SendSmsResponse resp = response.get();
        System.out.println(new Gson().toJson(resp));
        // Asynchronous processing of return values
        /*response.thenAccept(resp -> {
            System.out.println(new Gson().toJson(resp));
        }).exceptionally(throwable -> { // Handling exceptions
            System.out.println(throwable.getMessage());
            return null;
        });*/

        // Finally, close the client
        client.close();
    }

}

简单把这些代码整改一下:

java 复制代码
@Configuration
public class SMSConfig {

    @Value("${spring.cloud.alicloud.access-key}")
    private String accessId;
    @Value("${spring.cloud.alicloud.secret-key}")
    private String secretKey;

    @Bean
    public StaticCredentialProvider provider() {
       return StaticCredentialProvider.create(Credential.builder().accessKeyId(accessId).accessKeySecret(secretKey).build());
    }

}
java 复制代码
@RestController
public class SendSmsController {

    @Autowired
    private StaticCredentialProvider provider;

    /**
     * 提供接口,供别的服务调用
     *
     * @param phone
     * @param code
     * @return "body": {
     * "bizId": "774515119736291045^0",
     * "code": "OK",
     * "message": "OK",
     * "requestId": "D6BD5A90-8755-5C82-B631-0F40AB7B41B0"
     * }
     */
    @GetMapping("/sms/send")
    public R sendSms(@RequestParam("phone") String phone, @RequestParam("code") String code) throws ExecutionException, InterruptedException {

        AsyncClient client = AsyncClient.builder().region("cn-shanghai") // Region ID
                .credentialsProvider(provider).overrideConfiguration(ClientOverrideConfiguration.create().setEndpointOverride("dysmsapi.aliyuncs.com")).build();

        SendSmsRequest sendSmsRequest = SendSmsRequest.builder().signName("阿里云短信测试").templateCode("SMS_154950909").phoneNumbers(phone).templateParam("{\"code\":\"" + code + "\"}").build();

        CompletableFuture<SendSmsResponse> response = client.sendSms(sendSmsRequest);
        SendSmsResponse resp = response.get();

        /*
            {
                "headers": {
                    "Keep-Alive": "timeout\u003d25" ......
                },
                "statusCode": 200,
                "body": {
                    "bizId": "774515119736291045^0",
                    "code": "OK",
                    "message": "OK",
                    "requestId": "D6BD5A90-8755-5C82-B631-0F40AB7B41B0"
                }
            }
         */
        client.close();

        if (resp.getBody().getMessage().equalsIgnoreCase("OK")) return R.ok();
        return R.error(BizCodeEnume.SMS_SEND_EXCEPTION);
    }

}

邮件服务

xml 复制代码
<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4.1</version>
</dependency>
java 复制代码
@Data
public class EmailVo {
    private String receiveMail;
    private String subject;
    private String content;
}
java 复制代码
@Configuration
public class EmailConfig {

// 我在Nacos配置中心配的user和password
    @Value("${mail.user}")
    private String mailUser;

    @Value("${mail.password}")
    private String mailPassword;

    @Bean
    public Properties props() {
        // 创建Properties 类用于记录邮箱的一些属性
        Properties props = new Properties();
        // 表示SMTP发送邮件,必须进行身份验证
        props.put("mail.smtp.auth", "true");
        //此处填写SMTP服务器
        props.put("mail.smtp.host", "smtp.qq.com");
        //端口号,QQ邮箱端口587
        props.put("mail.smtp.port", "587");
        // 此处填写,写信人的账号
        props.put("mail.user", mailUser);
        // 此处填写16位STMP口令
        props.put("mail.password", mailPassword);
        return props;
    }

    @Bean
    public Authenticator authenticator(Properties props) {
        // 构建授权信息,用于进行SMTP进行身份验证
        return new Authenticator() {
            protected PasswordAuthentication getPasswordAuthentication() {
                // 用户名、密码
                String userName = props.getProperty("mail.user");
                String password = props.getProperty("mail.password");
                return new PasswordAuthentication(userName, password);
            }
        };
    }
}
java 复制代码
@RestController
public class SendEmailController {

    @Autowired
    private Properties props;

    @Autowired
    private Authenticator authenticator;

    @PostMapping("/email/send")
    public R sendEmail(@RequestBody EmailTo emailTo) throws MessagingException {
        // 使用环境属性和授权信息,创建邮件会话
        Session mailSession = Session.getInstance(props, authenticator);
        // 创建邮件消息
        MimeMessage message = new MimeMessage(mailSession);
        // 设置发件人
        InternetAddress form = new InternetAddress(props.getProperty("mail.user"));
        message.setFrom(form);
        // 设置收件人的邮箱
        InternetAddress to = new InternetAddress(emailTo.getReceiveMail());
        message.setRecipient(Message.RecipientType.TO, to);
        // 设置邮件标题
        message.setSubject(emailTo.getSubject());
        // 设置邮件的内容体
        message.setContent(emailTo.getContent(), "text/html;charset=UTF-8");
        // 最后当然就是发送邮件啦
        Transport.send(message);

        return R.ok();
    }

}

验证码

短信形式:

java 复制代码
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone) {
//        Redis缓存验证码:存起来方便下次校验 以及 可以给验证码设置有效期

        String code = getRandomCode().toString();

//        防止同一个手机号在60s内再次发送验证码
        String key = AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone;

        String oldCode = stringRedisTemplate.opsForValue().get(key);
        if (!StringUtils.isEmpty(oldCode)) {
            long l = Long.parseLong(oldCode.split("_")[1]);
            if (System.currentTimeMillis() - l < 60000) { // 如果时间间隔小于60s
                return R.error(BizCodeEnume.SMS_MULTI_EXCEPTION);
            }
        }

//        R r = thirdPartyFeignService.sendSms(phone, code);
//        if (r.getCode() == BizCodeEnume.SUCCESS.getCode()) {
//            code = code + "_" + System.currentTimeMillis();
//            stringRedisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES); //过期时间5分钟
//        }
//        return r;

        CompletableFuture.runAsync(() -> thirdPartyFeignService.sendSms(phone, code), threadPool);
        CompletableFuture.runAsync(() -> {
            stringRedisTemplate.opsForValue().set(key, codeResolve(code), 5, TimeUnit.MINUTES); //过期时间5分钟
        }, threadPool);
        return R.ok();
    }

生成验证码(随机四位数):

java 复制代码
    private Integer getRandomCode() {
        //4位数字验证码:想要[1000,9999],也就是[1000,10000)

        // Math.random() -> [0, 1)  // (int) Math.random()永远为0
        // Math.random() * (end - begin) -> [0, end - begin)
        // begin + Math.random() * (end - begin) -> [begin, end)
        int code = (int) (1000 + Math.random() * (10000 - 1000));
        return code;
    }

邮件形式:

java 复制代码
    @GetMapping("/sms/sendEmail")
    public R sendEmailCode(@RequestParam("email") String email) throws MessagingException {
        String code = UUID.randomUUID().toString().substring(0, 5);
        String key = AuthServerConstant.EMAIL_CODE_CACHE_PREFIX + email;

        String oldCode = stringRedisTemplate.opsForValue().get(key);
        if (!StringUtils.isEmpty(oldCode)) { // 说明5分钟内已经给该邮箱发送过验证码了
            long l = Long.parseLong(oldCode.split("_")[1]);
            if (System.currentTimeMillis() - l < 60000) { // 如果时间间隔小于60s
                return R.error(BizCodeEnume.SMS_MULTI_EXCEPTION);
            }
        }

        CompletableFuture.runAsync(() -> {
            // 给Redis放置验证码
            String realSaveCode = code + "_" + System.currentTimeMillis();
            stringRedisTemplate.opsForValue().set(key, realSaveCode, 5, TimeUnit.MINUTES); //过期时间5分钟
        }, threadPool);

        CompletableFuture.runAsync(() -> {
            // 发送邮件
            try {
                EmailTo emailTo = new EmailTo();
                emailTo.setReceiveMail(email);
                emailTo.setContent("验证码:" + code + "------有效期5分钟!");
                emailTo.setSubject("欢迎注册!");
                thirdPartyFeignService.sendEmail(emailTo);
            } catch (MessagingException e) {
                e.printStackTrace();
            }
        }, threadPool);

        return R.ok();
    }

异常机制

java 复制代码
    @PostMapping("/regist")
    public R regist(@RequestBody MemberRegistVo vo){
        try{
            memberService.regist(vo);
        }catch (PhoneExistException e){
            return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION);
        }catch (UsernameExistException e){
            return R.error(BizCodeEnume.USER_EXIST_EXCEPTION);
        }
        return R.ok();
    }
java 复制代码
    @Override
    public void regist(MemberRegistVo vo) {
        //检查用户名和手机号是否唯一。为了让controller能感知异常:异常机制
        String phone = vo.getPhone(); checkPhoneUnique(phone);
        String userName = vo.getUserName(); checkUsernameUnique(userName);

        MemberEntity entity = new MemberEntity();
        entity.setMobile(phone);
        entity.setUsername(userName);
        entity.setNickname(userName);

        //设置默认等级
        MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
        entity.setLevelId(levelEntity.getId());

        //密码要进行加密存储。//当然,也可以在前端就加密发过来
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());
        entity.setPassword(encode);

        //其他的默认信息
        //保存
        this.baseMapper.insert(entity);
    }
java 复制代码
    @Override
    public void checkPhoneUnique(String phone) throws PhoneExistException {
        Integer mobile = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (mobile > 0) {
            throw new PhoneExistException();
        }
    }

    @Override
    public void checkUsernameUnique(String username) throws UsernameExistException {
        Integer count = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
        if (count > 0) {
            throw new UsernameExistException();
        }
    }
java 复制代码
public class UsernameExistException extends RuntimeException {
    public UsernameExistException() {
        super("用户名存在");
    }
}

R:

java 复制代码
public class R extends HashMap<String, Object> {
    public static final String CODE = "code";
    public static final String MSG = "msg";
    public static final String DATA = "data";

    //利用fastjson进行逆转
    public <T> T getData(String key, TypeReference<T> typeReference) {
        Object data = get(key);// 默认是map
        String s = JSON.toJSONString(data); // 得转为JSON字符串
        T t = JSON.parseObject(s, typeReference);
        return t;
    }

    //利用fastjson进行逆转
    public <T> T getData(TypeReference<T> typeReference) {
        return getData(DATA, typeReference);
    }

    public R setData(Object data) {
        put(DATA, data);
        return this;
    }

    public R() {
        put(CODE, BizCodeEnume.SUCCESS.getCode());
        put(MSG, BizCodeEnume.SUCCESS.getMsg());
    }

    public static R error() {
        return error("服务器未知异常,请联系管理员");
    }

    public static R error(String msg) {
//        500
        return error(org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
    }

    public static R error(int code, String msg) {
        R r = new R();
        r.put(CODE, code);
        r.put(MSG, msg);
        return r;
    }

    public static R error(BizCodeEnume bizCodeEnume) {
        R r = new R();
        r.put(CODE, bizCodeEnume.getCode());
        r.put(MSG, bizCodeEnume.getMsg());
        return r;
    }

    public static R ok(String msg) {
        R r = new R();
        r.put(MSG, msg);
        return r;
    }

    public static R ok(Map<String, Object> map) {
        R r = new R();
        r.putAll(map);
        return r;
    }

    public static R ok() {
        return new R();
    }

    public R put(String key, Object value) {
        super.put(key, value);
        return this;
    }

    public Integer getCode() {
        return (Integer) this.get(CODE);
    }

    public String getMsg() {
        return (String) this.get(MSG);
    }
}
java 复制代码
/***
 * TODO 写博客
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为5位数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。
 *      10:通用             000:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 *  10: 通用
 *      001:参数格式校验
 *  11: 商品
 *  12: 订单
 *  13: 购物车
 *  14: 物流
 */
public enum BizCodeEnume {
    SUCCESS(0, "OK"),
    HTTP_SUCCESS(200, "OK"),

    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    TOO_MANY_REQUEST(10002,"请求流量过大"),
    SMS_MULTI_EXCEPTION(10003,"验证码获取频率太高,请1分钟后再试"),
    SMS_SEND_EXCEPTION(10004,"验证码发送失败"),
    SMS_CODE_EXCEPTION(10005,"验证码错误"),
    REG_ERROR_EXCEPTION(10006,"用户名或手机已存在,注册失败"),

    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    USER_EXIST_EXCEPTION(15001,"用户存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号存在"),
    NO_STOCK_EXCEPTION(21000,"商品库存不足"),
    LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"账号密码错误");

    private final int code;
    private final String msg;

    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

MD5

MD5:Message Digest algorithm 5,信息摘要算法

  • 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
  • 容易计算:从原数据计算出MD5值很容易。
  • 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
  • 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
  • 不可逆(即使知道加密算法,也不能反推出明文密码): MD5是一种信息摘要算法,会损 失元数据,所以不可逆出原数据是什么

但是,由于MD5的抗修改性和强抗碰撞(一个字符串的MD5值永远是那个值),发明了彩虹表(暴力 破解)。所以,MD5不能直接进行密码的加密存储

加盐:

  • 通过生成随机数与MD5生成字符串进行组合

  • 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可

    百度网盘的秒传:在上传文件之前,计算出该文件的MD5值,看有没有人之前上传过,也就是去匹配百度网盘的数据库中有没有相同的 MD5 值, 如果有一样的就不用传了

java 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallAuthApplicationTests {
    @Test
    public void contextLoads() {
        //MD5是不可逆的,但是利用它的抗修改性(一个字符串的MD5值永远是那个值),发明了彩虹表(暴力破解)。
        //所以,MD5不能直接进行密码的加密存储;
//        String s = DigestUtils.md5Hex("123456");

        //盐值加密;随机值 加盐 :$1$ + 8位字符
//        只要是同一个材料,做出来的饭是一样的,如果给饭里随机撒点"盐",那么,饭的口味就不一样了
        //"123456"+System.currentTimeMillis();

        //想要再次验证密码咋办?: 将密码再进行盐值(去数据库查当时保存的随机盐)加密一次,然后再去匹配密码是否正确
//        String s1 = Md5Crypt.md5Crypt("123456".getBytes()); //随机盐
//        String s1 = Md5Crypt.md5Crypt("123456".getBytes(),"$1$qqqqqqqq"); //指定盐
//        System.out.println(s1);

//        给数据库加字段有点麻烦,Spring有好用的工具:
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//        String encode = passwordEncoder.encode("123456");
//        $2a$10$coLmFyeppkTPTfD0RJgqL.nx33s0wvUmj.shqEM/6hvwOO4TWiGmy
//        $2a$10$4IP4F/2iFO2gbSvQKyJzGuI3RhU5Qdtr519KsyoXGAy.b7WT4P1RW
//        $2a$10$0hEI3vMkTbTqK76990MGu.s9QKrkjDSpgyhfzR4zsy07oKB9Jw.PS

//        System.out.println(encode);
//        boolean matches = passwordEncoder.matches("123456", "$2a$10$0hEI3vMkTbTqK76990MGu.s9QKrkjDSpgyhfzR4zsy07oKB9Jw.PS");
        boolean matches = passwordEncoder.matches("lpruoyu123", "$2a$10$m7TmOQAin5Tj6QzV1TT0ceW6iLypdN8LHkYP16DUEngJUfYNgWVEm");
        System.out.println(matches);
    }
}

参考

雷丰阳: Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目.


本文完,感谢您的关注支持!


相关推荐
节点。csn10 分钟前
Hadoop yarn安装
大数据·hadoop·分布式
NiNg_1_2341 小时前
基于Hadoop的数据清洗
大数据·hadoop·分布式
隔着天花板看星星3 小时前
Spark-Streaming集成Kafka
大数据·分布式·中间件·spark·kafka
Yvemil74 小时前
《开启微服务之旅:Spring Boot Web开发》(二)
前端·spring boot·微服务
维李设论4 小时前
Node.js的Web服务在Nacos中的实践
前端·spring cloud·微服务·eureka·nacos·node.js·express
jwolf26 小时前
基于K8S的微服务:一、服务发现,负载均衡测试(附calico网络问题解决)
微服务·kubernetes·服务发现
技术路上的苦行僧7 小时前
分布式专题(8)之MongoDB存储原理&多文档事务详解
数据库·分布式·mongodb
龙哥·三年风水7 小时前
workman服务端开发模式-应用开发-后端api推送修改二
分布式·gateway·php
小小工匠8 小时前
分布式协同 - 分布式事务_2PC & 3PC解决方案
分布式·分布式事务·2pc·3pc
Yvemil78 小时前
《开启微服务之旅:Spring Boot Web开发举例》(二)
前端·spring boot·微服务