美文网首页
微服务开发系列 第六篇:Redisson

微服务开发系列 第六篇:Redisson

作者: AC编程 | 来源:发表于2023-03-30 20:13 被阅读0次

    总概

    A、技术栈
    • 开发语言:Java 1.8
    • 数据库:MySQL、Redis、MongoDB、Elasticsearch
    • 微服务框架:Spring Cloud Alibaba
    • 微服务网关:Spring Cloud Gateway
    • 服务注册和配置中心:Nacos
    • 分布式事务:Seata
    • 链路追踪框架:Sleuth
    • 服务降级与熔断:Sentinel
    • ORM框架:MyBatis-Plus
    • 分布式任务调度平台:XXL-JOB
    • 消息中间件:RocketMQ
    • 分布式锁:Redisson
    • 权限:OAuth2
    • DevOps:Jenkins、Docker、K8S
    B、源码地址

    alanchenyan/ac-mall2-cloud

    C、本节实现目标
    • 用Redisson分布式锁控制并发
    D、系列

    一、实现用户积分功能

    1.1 功能说明

    用户下单后,自动给用户增加对应订单金额的积分数(取整)。

    1.2 用户积分表

    增加两张表,用户总积分表:t_member_integral、积分明细表:t_member_integral_log

    CREATE TABLE `t_member_integral` (
      `id` bigint NOT NULL COMMENT 'id',
      `member_id` bigint NOT NULL COMMENT '用户ID',
      `total_integral` bigint DEFAULT '0' COMMENT '用户总积分',
      `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除 0未删除 1已删除',
      `update_time` datetime NOT NULL COMMENT '修改时间',
      `create_time` datetime NOT NULL COMMENT '创建时间',
      PRIMARY KEY (`id`) USING BTREE,
      UNIQUE KEY `UK_member_id` (`member_id`),
      KEY `IDX_member_id` (`member_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户积分'
    
    CREATE TABLE `t_member_integral_log` (
      `id` bigint NOT NULL COMMENT 'id',
      `member_id` bigint NOT NULL COMMENT '用户ID',
      `integral` bigint DEFAULT '0' COMMENT '积分',
      `source_type` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '积分来源类型(下单奖励积分/签到积分)',
      `source_remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '积分来源描述(2023-02-23下单获得积分)',
      `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除 0未删除 1已删除',
      `update_time` datetime NOT NULL COMMENT '修改时间',
      `create_time` datetime NOT NULL COMMENT '创建时间',
      PRIMARY KEY (`id`) USING BTREE,
      KEY `IDX_member_id` (`member_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户积分明细'
    
    1.3 实现代码

    Controller

    @Api(tags = "用户")
    @RestController
    @RequestMapping("member")
    public class MemberController {
    
        @Resource
        private MemberService memberServiceImpl;
    
        @Resource
        private MemberIntegralComponent memberIntegralComponent;
    
        @ApiOperation(value = "记录积分")
        @PostMapping("integral")
        public Boolean recordIntegral(@RequestBody @Valid IntegralLogEditVO logEditVO) {
             return memberIntegralComponent.recordIntegral(logEditVO);
        }
    }
    

    Component

    @Component
    public class MemberIntegralComponent {
    
        @Resource
        private MemberIntegralLogService memberIntegralLogServiceImpl;
    
        @Resource
        private MemberIntegralService memberIntegralServiceImpl;
    
        /**
         * 记录积分
         * 并发问题:出现死锁
         * 并发下相同的业务参数去执行,第一个事物还没提交后面的事物又来了,这种我们加分布式锁就好了
         *
         * @param logEditVO
         */
        @Transactional(rollbackFor = Exception.class)
        public boolean recordIntegral(IntegralLogEditVO logEditVO) {
            //记录积分明细
            memberIntegralLogServiceImpl.addIntegral(logEditVO);
            //更新用户总积分
            memberIntegralServiceImpl.updateTotalIntegral(logEditVO.getMemberId());
            return true;
        }
    }
    

    MemberIntegralLogService

    @Slf4j
    @Service
    public class MemberIntegralLogServiceImpl implements MemberIntegralLogService {
    
        @Resource
        private MemberIntegralLogDao memberIntegralLogDaoImpl;
    
        @Override
        public void addIntegral(IntegralLogEditVO logEditVO) {
            MemberIntegralLog entity = new MemberIntegralLog();
            entity.setMemberId(logEditVO.getMemberId());
            entity.setIntegral(logEditVO.getIntegral());
            entity.setSourceType(logEditVO.getSourceType());
            entity.setSourceRemark(logEditVO.getSourceRemark());
            memberIntegralLogDaoImpl.save(entity);
        }
    }
    

    MemberIntegralService

    @Slf4j
    @Service
    public class MemberIntegralServiceImpl implements MemberIntegralService {
    
        @Resource
        private MemberIntegralDao memberIntegralDaoImpl;
    
        /**
         * 更新用户积分
         *
         * @param memberId
         */
        @Transactional(rollbackFor = Exception.class)
        @Override
        public void updateTotalIntegral(Long memberId) {
            if (!memberIntegralDaoImpl.existsMemberIntegral(memberId)) {
                MemberIntegral defaultEntity = new MemberIntegral();
                defaultEntity.setMemberId(memberId);
                defaultEntity.setTotalIntegral(0L);
                memberIntegralDaoImpl.save(defaultEntity);
            }
            memberIntegralDaoImpl.freshTotalIntegral(memberId);
        }
    }
    

    二、JMeter并发测试

    2.1 并发测试

    使用JMeter工具对积分接口进行并发测试,启动20个线程进行并发,如下图:

    积分接口 20个线程
    2.2 并发测试结果

    控制台显示死锁异常,20个线程数据没有全部正确执行成功。

    控制台显示死锁异常

    异常信息:

    Error updating database.  Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    

    查询数据库:


    用户总积分 积分明细

    三、Redisson分布式锁-控制并发

    3.1 maven加Redis依赖包

    在项目[mall-pom]的pom.xml里加入Redis依赖包

    <redisson.version>3.20.1</redisson.version>
    
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>${redisson.version}</version>
    </dependency>
    
    3.2 common.yml配置Redisson参数
    spring:
      redis:
        database: 0
        host: 127.0.0.1
        port: 6379
        password: 123abc
        jedis:
          pool:
            max-active: 500  #连接池的最大数据库连接数。设为0表示无限制
            max-idle: 20   #最大空闲数
            max-wait: -1
            min-idle: 5
        timeout: 1000
        redisson:
          password: 123abc
          cluster:
            nodeAddresses: ["redis://127.0.0.1:6379"]
          single:
            address: "redis://127.0.0.1:6379"
            database: 0
    

    common.yml完整配置

    spring:
      redis:
        database: 0
        host: 127.0.0.1
        port: 6379
        password: 123abc
        jedis:
          pool:
            max-active: 500  #连接池的最大数据库连接数。设为0表示无限制
            max-idle: 20   #最大空闲数
            max-wait: -1
            min-idle: 5
        timeout: 1000
        redisson:
          password: 123abc
          cluster:
            nodeAddresses: ["redis://127.0.0.1:6379"]
          single:
            address: "redis://127.0.0.1:6379"
            database: 0
    
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.100.51:3306/ac_db?serverTimezone=Asia/Shanghai&useUnicode=true&tinyInt1isBit=false&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
        username: ac_u
        password: ac_PWD_123
    
        #hikari数据库连接池
        hikari:
          pool-name: YH_HikariCP
          minimum-idle: 10 #最小空闲连接数量
          idle-timeout: 600000 #空闲连接存活最大时间,默认600000(10分钟)
          maximum-pool-size: 100 #连接池最大连接数,默认是10
          auto-commit: true  #此属性控制从池返回的连接的默认自动提交行为,默认值:true
          max-lifetime: 1800000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
          connection-timeout: 30000 #数据库连接超时时间,默认30秒,即30000
          connection-test-query: SELECT 1
    
    mybatis-plus:
      configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
    3.3 Redisson配置

    在[mall-core] 里加入配置代码

    @Data
    public class RedissonCluster {
    
        private List<String> nodeAddresses;
    }
    
    @Data
    public class RedissonSingle {
    
        private String address;
    
        private int database;
    }
    
    @Configuration
    @ConfigurationProperties(prefix = "spring.redis.redisson")
    @ConditionalOnProperty("spring.redis.redisson.password")
    @Data
    public class RedissonRepository {
    
        private String password;
    
        private RedissonCluster cluster;
    
        private RedissonSingle single;
    }
    
    import com.ac.core.properties.RedissonRepository;
    import lombok.extern.slf4j.Slf4j;
    import org.redisson.Redisson;
    import org.redisson.api.RedissonClient;
    import org.redisson.client.codec.StringCodec;
    import org.redisson.config.Config;
    import org.redisson.config.SingleServerConfig;
    import org.redisson.config.TransportMode;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.annotation.Resource;
    
    @Slf4j
    @Configuration
    public class RedissonConfig {
    
        @Resource
        private RedissonRepository redissonRepository;
    
        /**
         * Redisson单机配置
         *
         * @return
         */
        @Bean(destroyMethod = "shutdown")
        public RedissonClient singleRedisson() {
            log.info("redisSonRepository={}", redissonRepository);
            Config config = new Config();
            config.setCodec(StringCodec.INSTANCE);
            config.setTransportMode(TransportMode.NIO);
            SingleServerConfig singleServerConfig = config.useSingleServer();
            singleServerConfig.setPassword(redissonRepository.getPassword());
            singleServerConfig.setAddress(redissonRepository.getSingle().getAddress());
            singleServerConfig.setDatabase(redissonRepository.getSingle().getDatabase());
            return Redisson.create(config);
        }
    }
    
    3.4 接口加分布式锁

    Redisson分布式不能放在@Transactional里,否则会失效。

    @Api(tags = "用户")
    @RestController
    @RequestMapping("member")
    public class MemberController {
    
        @Resource
        private MemberService memberServiceImpl;
    
        @Resource
        private MemberIntegralComponent memberIntegralComponent;
    
        @Resource
        private RedissonClient redissonClient;
    
        @ApiOperation(value = "记录积分")
        @PostMapping("integral")
        public Boolean recordIntegral(@RequestBody @Valid IntegralLogEditVO logEditVO) {
            RLock redisLock = redissonClient.getLock("integral:" + logEditVO.getMemberId());
            try {
                redisLock.lock(5, TimeUnit.SECONDS);
                return memberIntegralComponent.recordIntegral(logEditVO);
            } finally {
                // 释放锁
                if (redisLock.isLocked() && redisLock.isHeldByCurrentThread()) {
                    redisLock.unlock();
                }
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:微服务开发系列 第六篇:Redisson

          本文链接:https://www.haomeiwen.com/subject/jjddrdtx.html