一、概述
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
数据库的悲观锁和乐观锁也能保证不同主机共享数据的一致性。但是却存在以下问题:
- 悲观锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
- 一旦悲观锁解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁
- 乐观锁适合读多写少的场景,如果在分布式场景下使用乐观锁,就会导致总是更新失败,效率极低。
还有别的实现锁的方案就不再赘述,redis是java的架构中很常见的技术
本文使用Redis分布式锁的开源项目redisson做分布式锁的简单实现。
redisson官网:分布式锁和同步器
二、准备工作
首先是必然要安装redis的。其次,我们要了解redisson锁的种类,为了配合锁的使用,使用了LockSupport和LockTest两个项目分别做商品服务和客户端测试,多个项目使用Consul做服务注册与发现。这里会简单介绍下LockSupport、LockRedis和LockTest三个项目是如何配合使用分布式锁的。
2.1 redisson锁的种类
redisson为我们提供了很多种锁,如:
可重入锁(Reentrant Lock) :基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
公平锁(Fair Lock) :基于Redis的Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。
联锁(MultiLock) :基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。
红锁(RedLock) :基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。
读写锁(ReadWriteLock) :基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
信号量(Semaphore) :基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
可过期性信号量(PermitExpirableSemaphore) :基于Redis的Redisson可过期性信号量(PermitExpirableSemaphore)是在RSemaphore对象的基础上,为每个信号增加了一个过期时间。每个信号可以通过独立的ID来辨识,释放时只能通过提交这个ID才能释放。它提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
闭锁(CountDownLatch) :基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。
锁的种类虽多,但是使用方法都是类似的,本文就选择公平锁来测试使用。
三、分布式锁的使用
下面详细介绍LockRedis是如何使用redisson做分布式锁的。
3.1 引入依赖
需要引入数据库相关jar、jpa、spring-boot-starter-data-redis、redisson-spring-boot-starter;
下面是我用写的一个切面类防重复提交,用了Redisson的RLock,关键方法是
tryLock 和 releaseLock
package org.jeecg.config.filter;
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson2.JSON;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
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.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.IpUtils;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.modules.system.entity.CrmSysConfig;
import org.jeecg.modules.system.service.ICrmSysConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
// 切面类
@Aspect
@Component
public class DuplicateSubmitAspect {
@Autowired
private RedisUtil redisUtil;
@Autowired
private ICrmSysConfigService crmSysConfigService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
@Autowired
private Environment environment;
// 使用 ThreadLocal 存储当前线程的 RLock,用于安全释放锁
private static final ThreadLocal<RLock> currentLock = new ThreadLocal<>();
private static final long TIMEOUT = 1; // 查询类接口超时时间(秒)
private static final long TIMEOUT_UPDATE = 5; // 更新类接口超时时间(秒)
/**
* 定义切点Pointcut
*/
@Pointcut("execution(public * org.jeecg.modules..*.*Controller.*(..))")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Class<?> targetClass = joinPoint.getTarget().getClass();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
PreventDuplicateSubmit methodIgnoreTenant = method.getAnnotation(PreventDuplicateSubmit.class);
//判断类上是否有注解
boolean isClassAnnotated = AnnotationUtils.isAnnotationDeclaredLocally(PreventDuplicateSubmit.class, targetClass);
//判断方法上是否有注解
boolean isMethodAnnotated = Objects.nonNull(methodIgnoreTenant);
// 如果有注解
if (isClassAnnotated || isMethodAnnotated) {
return joinPoint.proceed(); // 执行目标方法
}
//获取request
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String url = request.getRequestURI();
String token = request.getHeader("Authorization");
if(StringUtils.isBlank( token)){
try {
//获取IP地址
token = IpUtils.getIpAddr(request);
} catch (Exception e) {
token = "127.0.0.1";
}
}
// 否则,执行防止重复提交的逻辑
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
String lastPart = url.substring(url.lastIndexOf("/") + 1);
// 判断是否为更新类操作
boolean isUpdateMethod = (lastPart.contains("add") || lastPart.contains("edit")
|| lastPart.contains("delete") || lastPart.contains("update")
|| lastPart.contains("save") || lastPart.contains("remove")
|| lastPart.contains("create") || lastPart.contains("modify")
|| lastPart.contains("submit") || lastPart.contains("Add")
|| lastPart.contains("Edit") || lastPart.contains("Delete")
|| lastPart.contains("Update") || lastPart.contains("Save")
|| lastPart.contains("Remove") || lastPart.contains("Create")
|| lastPart.contains("Modify") || lastPart.contains("Submit"));
// 获取请求参数(用于更新类操作的重复提交判断)
String paramsStr = "";
if (isUpdateMethod) {
// 更新类操作:将请求参数加入到 key 中,相同参数视为重复提交
paramsStr = getRequestParams(joinPoint, request);
}
// 获取服务名
String serviceName = environment.getProperty("spring.application.name", "unknown-service");
// 生成唯一 key:服务名 + 用户名 + URL + token + 参数(仅更新类操作)
String keyBase = "";
if (null == sysUser || StringUtils.isBlank(sysUser.getUsername())) {
keyBase = serviceName + "-" + request.getRequestURL().toString() + "-" + token;
} else {
keyBase = serviceName + "-" + sysUser.getUsername() + "-" + request.getRequestURL().toString() + "-" + token;
}
// 更新类操作:将参数也加入到 key 中
if (isUpdateMethod && StringUtils.isNotBlank(paramsStr)) {
keyBase += "-" + paramsStr;
}
// 更新类接口:使用锁来防止重复提交
if (isUpdateMethod) {
String lockKey = "duplicate_submit_lock:" + DigestUtil.sha256Hex(keyBase);
long timeout = TIMEOUT_UPDATE;
// 使用分布式锁:原子操作 setIfAbsent
Boolean lockAcquired = tryLock(lockKey, timeout);
if (Boolean.TRUE.equals(lockAcquired)) {
// 获取锁成功,执行业务逻辑
try {
// 更新类接口的锁不立即释放,让它自然过期(5秒),确保5秒内相同参数的请求只能执行一次
Object result = joinPoint.proceed();
// 更新类接口:即使不释放锁,也要清理 ThreadLocal(防止内存泄漏)
cleanupLockIfNeeded();
return result;
} catch (Throwable e) {
// 业务逻辑执行异常,也要释放锁
releaseLock(lockKey);
throw e;
}
} else {
// 获取锁失败,说明有重复提交
throw new IllegalArgumentException("操作过于频繁,请稍等再试。");
}
} else {
// 查询类接口:不使用锁,直接使用计数器限制频率(允许并发访问)
String countKey = "duplicate_submit_count:" + DigestUtil.sha256Hex(keyBase);
long timeout = TIMEOUT;
// 使用 Redis 的 INCR 操作原子性地增加计数
Long callNum;
if (redisTemplate != null) {
callNum = redisTemplate.opsForValue().increment(countKey);
// 如果是第一次访问,设置过期时间
if (callNum != null && callNum == 1) {
redisTemplate.expire(countKey, timeout, TimeUnit.SECONDS);
}
} else {
// 降级方案:使用 RedisUtil(非原子,但兼容性更好)
Object countObj = redisUtil.get(countKey);
Integer currentCount = (countObj != null) ? Integer.valueOf(countObj.toString()) : 0;
callNum = (long) (currentCount + 1);
redisUtil.set(countKey, callNum, timeout);
}
CrmSysConfig duplicateSubmitCount = crmSysConfigService.getCrmConfigByCode("duplicate_submit_count");
Integer count = 5; // 限制次数
if(duplicateSubmitCount != null && StringUtils.isNotBlank(duplicateSubmitCount.getValue())){
try {
count = Integer.parseInt(duplicateSubmitCount.getValue());
} catch (Exception e) {
e.printStackTrace();
}
}
// 查询类接口:允许在限制次数内重复调用
if (callNum != null && callNum <= count) {
// 允许执行
return joinPoint.proceed();
} else {
// 超过限制次数,拒绝请求
throw new IllegalArgumentException("操作过于频繁,请稍等再试。");
}
}
}
/**
* 尝试获取分布式锁(优先使用 RLock,降级到 setIfAbsent)
* @param lockKey 锁的 key
* @param timeout 锁的过期时间(秒)
* @return true 表示获取锁成功,false 表示获取锁失败
*/
private Boolean tryLock(String lockKey, long timeout) {
if (redissonClient != null) {
// 优先使用 Redisson 的 RLock(支持看门狗、可重入、安全释放)
try {
RLock lock = redissonClient.getLock(lockKey);
// 尝试获取锁,等待时间0秒,持有时间timeout秒
boolean success = lock.tryLock(0, timeout, TimeUnit.SECONDS);
if (success) {
// 将锁存储到 ThreadLocal,用于后续安全释放
currentLock.set(lock);
}
return success;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
} else if (redisTemplate != null) {
// 降级方案:使用 RedisTemplate 的原子操作 setIfAbsent
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
Boolean success = ops.setIfAbsent(lockKey, "1", timeout, TimeUnit.SECONDS);
return success;
} else {
// 最后的降级方案:如果 RedisTemplate 不可用,使用 RedisUtil(非原子,但兼容性更好)
Object obj = redisUtil.get(lockKey);
if (obj == null) {
redisUtil.set(lockKey, "1", timeout);
return true;
}
return false;
}
}
/**
* 释放分布式锁(优先使用 RLock,安全释放)
* @param lockKey 锁的 key
*/
private void releaseLock(String lockKey) {
if (redissonClient != null) {
// 使用 RLock 安全释放锁(只有持有锁的线程才能释放)
RLock lock = currentLock.get();
if (lock != null && lock.isHeldByCurrentThread()) {
try {
lock.unlock();
} catch (Exception e) {
// 锁可能已经过期或已被释放,忽略异常
} finally {
currentLock.remove();
}
} else {
// 如果锁已过期或不是当前线程持有,也要清理 ThreadLocal
currentLock.remove();
}
} else if (redisTemplate != null) {
redisTemplate.delete(lockKey);
} else {
redisUtil.del(lockKey);
}
}
/**
* 清理 ThreadLocal 中的锁引用(用于更新类接口,锁自然过期的情况)
*/
private void cleanupLockIfNeeded() {
if (redissonClient != null) {
RLock lock = currentLock.get();
if (lock != null) {
// 如果锁已过期或不是当前线程持有,清理 ThreadLocal
if (!lock.isHeldByCurrentThread()) {
currentLock.remove();
}
}
}
}
/**
* 获取请求参数的字符串表示(用于生成唯一 key)
* @param joinPoint AOP 切点
* @param request HTTP 请求
* @return 请求参数的 JSON 字符串
*/
private String getRequestParams(ProceedingJoinPoint joinPoint, HttpServletRequest request) {
try {
String httpMethod = request.getMethod();
String params = "";
// POST/PUT/PATCH 请求:从方法参数获取
if ("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod)) {
Object[] paramsArray = joinPoint.getArgs();
Object[] arguments = new Object[paramsArray.length];
for (int i = 0; i < paramsArray.length; i++) {
// 排除不能序列化的对象
if (paramsArray[i] instanceof BindingResult
|| paramsArray[i] instanceof ServletRequest
|| paramsArray[i] instanceof ServletResponse
|| paramsArray[i] instanceof MultipartFile) {
continue;
}
arguments[i] = paramsArray[i];
}
// 序列化为 JSON 字符串
if (arguments.length > 0) {
params = JSON.toJSONString(arguments);
}
} else {
// GET/DELETE 请求:从 URL 参数获取
String queryString = request.getQueryString();
if (StringUtils.isNotBlank(queryString)) {
params = queryString;
}
}
return params;
} catch (Exception e) {
// 如果获取参数失败,返回空字符串(不影响基本功能)
return "";
}
}
}