Redisson分布式锁
小编最近在排查一个流水编号重复问题的BUG,使用到Redisson分布式锁,今天有时间就特意写下文档记录一下.
问题分析
- 首先简单说一个流水号的设计思路:
通过mysql数据库表记录流水号,表中主要有几个关键字段大致如下:
flag
varchar(50),version
int DEFAULT NULL,num
int DEFAULT NULL,`
flag表示关键字; version表示版本; num是我们的流水号
根据flag获取表中的最大num,然后进行+1操作,并在表中新增一条数据(哎,其实通过version进行更新就可,为了记录这次问题我就按照这种操作模拟了一下)
- 代码分析:
最初是通过synchronized关键字锁住代码块获取表中最大的num编号,然后在进行insert数据库操作;
上面的操作不难发现问题,通过synchronized关键字锁,在单机情况下并发不高还能玩玩,但是如果在集群或者高并发情况下就会出现重复编号的问题.(说到这里有人可能提出疑问,你用mysql记录最大编号,如果分库分表或者多数据源了怎么办???,给大家抛出来一个问题环境评论区留言)
- Redisson
针对上面的问题,我首先想到的就是通过分布式锁来解决这种问题,让我直接想到了Redisson框架,封装好的给予redis的分布式锁框架,简单好用;
springboot+redisson实现
- 首先引入配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.1</version>
</dependency>
- 配置Redisson
package com.jsoft.per.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
1. @Author: swang
2. @Description:
3. @Date: 2023/5/3 下午7:13
*/
@Configuration
public class RedisConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient(){
// 创建配置 指定redis地址及节点信息
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
- aop切面配置Redisson
简单说一下为啥采用aop切面加锁: 我的流水号操作在事物方法中使用, spring的事物底层是采用动态代理方式实现的, 按照正常的加锁就会出现一种情况: 当我的redisson锁释放了但是我的事物还没有提交,在并发情况就会导致重复编号问题, 所以加锁采用aop切面方式,并设置优先级高于事物 ----> 事物提交后我在解锁.
代码如下:
自定义注解
package com.jsoft.per.annotation;
import java.lang.annotation.*;
/**
* @Author: swang
* @Description:
* @Date: 2023/5/3 下午10:29
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisLock {
String lockName() default "num";
}
切面实现
package com.jsoft.per.aspectj;
import com.jsoft.per.annotation.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
/**
* @Author: swang
* @Description:
* @Date: 2023/5/3 下午10:31
*/
@Aspect
@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class LockAsp {
@Resource
private RedissonClient redissonClient;
@Pointcut("@annotation(com.jsoft.per.annotation.RedisLock)")
public void pointCut() {
}
@Around("pointCut()")
public Object doLock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedisLock lock = method.getAnnotation(RedisLock.class);
String lockName = lock.lockName();
RLock rLock = redissonClient.getLock(lockName);
try {
rLock.lock();
joinPoint.proceed();
} catch (Exception e) {
e.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
rLock.unlock();
}
return joinPoint.proceed();
}
}
使用方式
@Override
@Transactional(rollbackFor = Exception.class)
public void save() {
StockPO po = StockPO.builder()
.name("白菜")
.num(100)
.build();
stockMapper.insert(po);
String key = "";
stockService.getNum(key);
}
/**
* @Description 获取流水号
**/
@Override
@RedisLock(lockName = "lock")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void getNum(String key) {
int maxNum = getMaxNum();
NumPO po = NumPO.builder()
.flag("KC")
.version(1)
.num(maxNum)
.build();
numMapper.insert(po);
}
private int getMaxNum() {
QueryWrapper<NumPO> queryWrapper = new QueryWrapper();
queryWrapper.eq("flag", "KC");
queryWrapper.orderByDesc("num");
List<NumPO> list = numMapper.selectList(queryWrapper);
if(!CollectionUtils.isEmpty(list)) {
NumPO numPO = list.stream().findFirst().get();
return numPO.getNum() + 1;
} else {
return 1;
}
}
以上代码是我写的一个简单案例, 都已经通过压测,并发情况下也没有出现重复编号,请大胆验证;
细心的人会发现,我加锁的名称是写死的,那如果都用这个注解加锁的时候,是不是就会出现一个问题,不同的业务加锁时候使用的是同一个名称,就会导致我的锁竞争比较大. 有问题就有解决方案.
- 配置sePL表达式
private final SpelExpressionParser parser = new SpelExpressionParser();
private final LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
private String getSPL(ProceedingJoinPoint joinPoint, String spl) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String[] parameterNames = nameDiscoverer.getParameterNames(method);
StandardEvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
return Objects.requireNonNull(parser.parseExpression(spl).getValue(context, String.class));
}
最终注解使用方式如下:
@Override
@RedisLock(lockName = "lock", spEL = "#key")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void getNum(String key) {
int maxNum = getMaxNum();
NumPO po = NumPO.builder()
.flag("KC")
.version(1)
.num(maxNum)
.build();
numMapper.insert(po);
}