总概
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个线程进行并发,如下图:
积分接口
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();
}
}
}
}