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

总概

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、本节实现目标

  • 用Redisson分布式锁控制并发

一、实现用户积分功能

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个线程进行并发,如下图:

扫描二维码关注公众号,回复: 15205113 查看本文章

积分接口

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();
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/quanzhan_King/article/details/130852203