概述
使用自定义注解+ Spring AOP 切面编程 封装分布式锁逻辑,并通过抽象分布式锁工厂,解耦分布式锁具体实现,可以通过配置来动态切换具体分布式锁实例
初始化配置
spring:profiles:active: dev#redis配置redis:database: 0host: xxxport: 6379timeout: 5000password: xxx#分布式锁+限流redisson:single-server:address: redis://xxxpassword: xxxdatabase: 0timeout: 3000distributed:lock:type: "redisson" # 启用Redisson实现redisson:default-wait-time: 10 # 覆盖默认等待时间default-lease-time: 30 # 覆盖默认租约时间default-lock-type: FAIR # 默认使用公平锁
redis:arrange:type: "single" # 可选 single/cluster
redisson配置类
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.ClusterServersConfig;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;@Configuration
//条件注解,只有当distributed.lock.type属性值为redisson时,该配置类才会生效
@ConditionalOnProperty(name = "distributed.lock.type", havingValue = "redisson")
public class RedissonConfig {//单例模式@Bean(name = "redissonClient")@ConditionalOnProperty(name = "redis.arrange.type", havingValue = "single")public RedissonClient singleRedissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://xxx").setTimeout(3000).setPassword("151212").setDatabase(0);return Redisson.create(config); //注意这里要返回redissonClient,和名称对应,因为@Resource会按名称找到bean注入}//集群模式@Bean(name = "redissonClient")@ConditionalOnProperty(name = "redis.arrange.type", havingValue = "cluster")public RedissonClient clusterRedissonClient(){Config config = new Config();ClusterServersConfig clusterServersConfig = config.useClusterServers();clusterServersConfig.setNodeAddresses(Arrays.asList("redis://xxxx"));return Redisson.create(config);}
}
通用锁接口
对外提供通用的分布式锁基本操作
public interface DistributedLock {boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException; //尝试加锁 非阻塞void lock(long leaseTime, TimeUnit unit); //加锁 阻塞void unlock(); //解锁boolean isLocked(); //检查锁的状态boolean isHeldByThread(long threadId); //锁是否被指定线程持有boolean isHeldByCurrentThread(); //锁是否被当前线程持有
}
工厂接口
对外提供统一的 获取分布式锁入口,不需要关心内部的锁实现
public interface DistributedLockFactory {/*** 根据key和锁类型 获取分布式锁*/DistributedLock getDistributedLock(String key, String lockType);
}
工厂实现类
工厂实现类就是基于redisson封装了创建分布式锁的工厂类逻辑,后续可以有多个实现类,比如基于zookeeper的
import io.lettuce.core.RedisException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;@Component
//条件注解,表示只有当配置文件中的属性 `distributed.lock.type` 的值为redisson时,这个类才会被加载到 Spring 容器中
//允许在不同的环境中切换不同的分布式锁实现(例如 Redisson、Zookeeper等)(如测试环境降级为本地锁),提升环境适配能力
//例如,ZooKeeper 的锁可能需要不同的检查逻辑,但通过接口可以统一对外暴露
@ConditionalOnProperty(name = "distributed.lock.type", havingValue = "redisson")
@Slf4j
public class RedissonLockFactory implements DistributedLockFactory {private final Logger logger = LoggerFactory.getLogger(RedissonLockFactory.class);@Resourceprivate RedissonClient redissonClient;// 从配置文件读取默认值(带默认值)@Value("${distributed.lock.redisson.default-wait-time:10}")private long defaultWaitTime;@Value("${distributed.lock.redisson.default-lease-time:30}") private long defaultLeaseTime;@Value("${distributed.lock.redisson.default-lock-type:FAIR}")private String defaultLockType;// 根据配置选择锁类型private RLock chooseLockType(String key, String lockType) {return "FAIR".equalsIgnoreCase(lockType) ?redissonClient.getFairLock(key) :redissonClient.getLock(key);}//根据key获取分布式锁@Overridepublic DistributedLock getDistributedLock(String key, String lockType) {// 根据配置选择锁类型RLock rLock = chooseLockType(key,lockType);//匿名内部类封装:避免了开发者在业务中直接操作`RLock`对象,防止因误调用`unlock()`导致锁泄漏或跨线程释放问题return new DistributedLock() {@Overridepublic boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {if (waitTime < 0 || leaseTime < 0) {throw new IllegalArgumentException("参数无效");}// 约定:传入0,使用默认值处理 传入具体值,则使用用户自定义值//实现了默认值与用户自定义值的动态切换long actualWait = waitTime == 0 ? defaultWaitTime : waitTime;long actualLease = leaseTime == 0 ? defaultLeaseTime : leaseTime;//非阻塞 尝试加锁boolean success = rLock.tryLock(actualWait, actualLease, unit);logger.info("{} 尝试加锁 结果:{}", key, success);return success;}//阻塞当前线程直到成功上锁@Overridepublic void lock(long leaseTime, TimeUnit unit) {long actualLease = leaseTime == 0 ? defaultLeaseTime : leaseTime;rLock.lock(actualLease, unit);logger.debug("{} 成功加锁", key);}@Overridepublic void unlock() {// 使用一次性检查,减少竞态条件try {if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {logger.debug("Unlocking key: {}", key);rLock.unlock();} else {logger.warn("试图解锁当前线程未持有的锁: {}", key);}} catch (RedisException e) {logger.error("释放锁时发生异常: {}", key, e);}}//判断当前锁的状态@Overridepublic boolean isLocked() {return rLock.isLocked();}//判断指定线程是否持有锁@Overridepublic boolean isHeldByThread(long threadId) {return rLock.isHeldByThread(threadId);}//判断当前线程是否持有锁@Overridepublic boolean isHeldByCurrentThread() {return rLock.isHeldByCurrentThread();}};}
}
为什么使用匿名内部类?
主要原因为了快速实现接口,并将其直接作为方法的返回值,无需单独定义一个具体的实现类,减少创建类的数量
直接通过匿名内部类实现了 DistributedLock
接口,并将 Redisson 的锁功能封装在其中,可以将 Redisson 的具体实现封装起来
另外就是所有与分布式锁相关的逻辑都集中在 工厂实现类中,便于维护和管理。
而且考虑到分布式锁其实也没有那么多复杂方面,所以就没考虑单独写实现类了
与直接使用 Redisson 的对比
对比维度 | 直接使用 Redisson 的 RLock | 封装后的调用方式 |
---|---|---|
依赖关系 | 业务代码直接依赖 Redisson 的具体类(RLock 、RedissonClient )。 | 业务代码仅依赖抽象接口 DistributedLock ,与具体实现解耦。 |
扩展性 | 若需切换实现(如 ZooKeeper),需修改所有调用 Redisson 的代码。 | 通过配置即可切换实现,无需修改业务代码。 |
配置管理 | Redisson 的配置(如连接参数、超时时间)分散在业务代码中。 | Redisson 的配置集中管理在 Spring 配置文件中,统一维护。 |
日志与监控 | 无内置日志记录锁的获取状态,需手动添加日志。 | 封装后的 tryLock 方法自动记录锁的获取结果(如 logger.info )。 |
安全性和健壮性 | unlock 方法可能被误释放(未检查是否持有锁) 1. 开发者可能在未确认锁状态的情况下直接调用了 unlock() 2. 如果在加锁和解锁之间发生了异常,可能会导致锁的状态不一致 | 封装后的 unlock 方法会检查锁是否由当前线程持有,避免误释放。 |
可维护性 | 代码耦合度高,难以维护和测试。 | 代码更清晰,便于维护和单元测试(可通过 Mock DistributedLock 接口)。 |
未来扩展 | 新增功能(如锁统计、重试机制)需修改所有调用处。 | 新增功能可通过扩展接口和工厂实现,业务代码无需改动。 |
直接使用 Redisson 的
RLock
时
@Autowired
private RedissonClient redissonClient;public void someMethod() {RLock rLock = redissonClient.getLock("myKey");try {// 直接调用 Redisson 的 tryLockif (rLock.tryLock(10, 30, TimeUnit.SECONDS)) {// 执行业务逻辑System.out.println("Lock acquired, performing operations...");}} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {// 直接调用 Redisson 的 unlockrLock.unlock();}
}
封装后的解决方案:
@Autowired
private DistributedLockFactory lockFactory;public void someMethod() {// 通过工厂获取分布式锁实例DistributedLock lock = lockFactory.getDistributedLock("myKey");// 尝试加锁(使用默认值)调用匿名内部类封装好的tryLock方法if (lock.tryLock(0, 0, TimeUnit.SECONDS)) {try {// 执行业务逻辑System.out.println("Lock acquired, performing operations...");} finally {// 确保释放锁lock.unlock();}} else {System.out.println("Failed to acquire lock.");}
}
总结
通过抽象接口 DistributedLock
和工厂模式 DistributedLockFactory
,可以实现以下优势:
-
依赖接口而非实现:业务代码仅依赖
DistributedLock
接口,与具体实现(Redisson、ZooKeeper 等)解耦。 -
动态切换实现:通过
@ConditionalOnProperty
注解,可以在 Spring Boot 的配置文件中动态指定锁的类型(如distributed.lock.type=redisson
或zookeeper
)。Spring 容器会根据配置加载对应的工厂类(如RedissonLockFactory
或ZooKeeperLockFactory
),而业务代码无需任何修改。-
添加新实现:实现
DistributedLockFactory
的子类ZooKeeperLockFactory
-
配置切换:在
application.properties
中修改配置 -
distributed.lock.type=zookeeper
-
-
零侵入性:业务代码只需通过
DistributedLockFactory
获取锁实例,无需感知具体实现。
AOP封装 分布式注解
自定义注解
import java.lang.annotation.*;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {/*** 锁key(支持SpEL表达式) * 示例:"order:lock:#{#order.id}"*/String key()default "";/*** 锁等待时间(秒)* 默认值0表示使用配置文件中的默认等待时间*/long waitTime() default 0; /*** 锁持有时间(秒)* 默认值0表示使用配置文件中的默认租约时间*/long leaseTime() default 0; /*** 锁类型(支持NORMAL/FAIR)* 默认值为NORMAL*/String lockType() default "NORMAL"; // 覆盖配置中的默认值//加锁失败策略 默认抛异常LockFailStrategy failStrategy() default LockFailStrategy.THROW_EXCEPTION;enum LockFailStrategy {THROW_EXCEPTION, // 抛出异常RETURN_NULL, // 返回空值RETRY // 重试(需配合重试机制)}
}
AOP切面
import io.lettuce.core.RedisException;
import io.lettuce.core.dynamic.support.ParameterNameDiscoverer;
import io.lettuce.core.dynamic.support.StandardReflectionParameterNameDiscoverer;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Param;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;import java.util.concurrent.TimeUnit;@Aspect
@Component
@Slf4j
public class DistributedLockAspect {// 分布式锁工厂接口,用于获取分布式锁实例private final DistributedLockFactory lockFactory;// SpEL表达式解析器,用于解析动态keyprivate final ExpressionParser parser = new SpelExpressionParser();//构造器注入分布式锁工厂@Autowiredpublic DistributedLockAspect(DistributedLockFactory lockFactory) {this.lockFactory = lockFactory;}/*** 环绕通知,匹配带有RedisLock注解的方法** @param joinPoint 方法切入点* @param redisLock 注解对象* @return 方法执行结果* @throws Throwable 抛出异常*/@Around("@annotation(redisLock)")public Object aroundLock(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {//从ProceedingJoinPoint对象中获取目标方法的签名,并将其强制转换为MethodSignature类型MethodSignature signature = (MethodSignature) joinPoint.getSignature();//获取目标对象的方法Method method = signature.getMethod();//获取目标方法的参数数组Object[] args = joinPoint.getArgs();String lockKey = redisLock.key();if ("".equals(lockKey)) {//根据当前的类名+方法参数信息生成keylockKey = configKey(signature.getDeclaringType(), method).replaceAll("[^a-zA-Z0-9]", "");System.out.println(lockKey);} else {//支持SpEL表达式StandardEvaluationContext context = new StandardEvaluationContext();//将当前方法参数信息都存入到SpEl执行的上下文中DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();String[] parameterNames = discoverer.getParameterNames(method);for (int i = 0, len = parameterNames.length; i < len; i++) {context.setVariable(parameterNames[i], args[i]);}Expression expression = parser.parseExpression(lockKey);lockKey = expression.getValue(context, String.class);}// 获取锁类型(NORMAL/FAIR)String lockType = redisLock.lockType();// 通过工厂获取分布式锁实例DistributedLock lock = lockFactory.getDistributedLock(lockKey, lockType);// 处理等待时间和租约时间的默认值long waitTime = redisLock.waitTime();long leaseTime = redisLock.leaseTime();boolean locked = false;try {locked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS); // 尝试获取锁if (!locked) {// 未获取到锁,执行失败策略return handleLockFailure(redisLock.failStrategy(), method);}log.info("线程 {} 成功获取锁 [{}]", Thread.currentThread().getName(), lockKey);return joinPoint.proceed(); // 执行目标方法}finally {lock.unlock(); // 确保释放锁log.info("线程 {} 释放锁 [{}]", Thread.currentThread().getName(), lockKey);}}private String configKey(Class<?> targetType, Method method) {StringBuilder builder = new StringBuilder();builder.append(targetType.getSimpleName());builder.append('#').append(method.getName()).append('(');for (Class<?> param : method.getParameterTypes()){builder.append(param.getSimpleName()).append(',');}if (method.getParameterTypes().length > 0){builder.deleteCharAt(builder.length() - 1);}return builder.append(')').toString();}//处理加锁失败策略private Object handleLockFailure(RedisLock.LockFailStrategy strategy, Method method) throws Exception {switch (strategy) {case THROW_EXCEPTION:// 抛出异常throw new IllegalArgumentException("未能获取分布式锁");case RETURN_NULL:// 返回nulllog.warn("方法 {} 加锁失败,返回空值", method.getName());return null;case RETRY:// 重试逻辑(需集成Spring Retry实现)log.error("重试策略尚未实现");throw new UnsupportedOperationException("重试策略尚未实现");default:// 不支持的策略throw new IllegalArgumentException("不支持的锁失败策略");}}
}
使用案例
@RestController
@RequestMapping("/esbook")
public class EsBookController {@ResourceEsBookService esBookService;@GetMapping("/stock")@RedisLock(key = "'esbook:'+ #id", // 动态生成锁keywaitTime = 30, // 等待时间30秒leaseTime = 5, // 租约时间5秒lockType = "FAIR", // 使用公平锁failStrategy = RedisLock.LockFailStrategy.RETURN_NULL // 失败返回null)public String updateBookStoreNum(@RequestParam Integer id) {String s = esBookService.updateBookStoreNum(id);return s;}
}
@Service
public class EsBookServiceImpl extends ServiceImpl<EsBookMapper, EsBook>implements EsBookService{@Overridepublic String updateBookStoreNum(Integer id) {// 从数据库中获取图书库存EsBook book = baseMapper.selectById(id);if (book != null && book.getStoreNum() >=1) {// 更新库存book.setStoreNum(book.getStoreNum() - 1);baseMapper.updateById(book);System.out.println("库存更新成功,当前库存: " + book.getStoreNum());return "Success";}return "库存不足!";}
}
JMETER压测
JDBC Connection [HikariProxyConnection@844857863 wrapping com.mysql.cj.jdbc.ConnectionImpl@6cb120b2] will not be managed by Spring
==> Preparing: UPDATE es_book SET name=?, cover=?, description=?, author=?, publisher=?, price=?, store_num=?, status=?, category_id=? WHERE id=?
==> Parameters: 别做傻瓜(String), bookcover/别做傻瓜.jpg(String), 这本书由著名美学家朱光潜编写,内容探讨了如何保持理性与清醒,避免做出愚昧决策,特别是在面对生活困境时如何保持自我判断的能力。书中结合了生活中的实际案例,强调反思和批判性思维的重要性。(String), 朱光潜(String), 北京出版社(String), 35.0(Double), 0(Integer), 0(Integer), 6(Integer), 1(Integer)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4bd54fcd]
库存更新成功,当前库存: 0
2025-04-05 12:35:17.765 INFO 25860 --- [io-8111-exec-28] c.z.bean.common.DistributedLockAspect : 线程 http-nio-8111-exec-28 释放锁 [esbook:1]
2025-04-05 12:35:17.799 INFO 25860 --- [io-8111-exec-29] c.z.bean.common.RedissonLockFactory : esbook:1 尝试加锁 结果:true
2025-04-05 12:35:17.799 INFO 25860 --- [io-8111-exec-29] c.z.bean.common.DistributedLockAspect : 线程 http-nio-8111-exec-29 成功获取锁 [esbook:1]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@307f6246] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1052281401 wrapping com.mysql.cj.jdbc.ConnectionImpl@6cb120b2] will not be managed by Spring
==> Preparing: SELECT id,name,cover,description,author,publisher,price,store_num AS storeNum,status,category_id FROM es_book WHERE id=?
==> Parameters: 1(Integer)
<== Columns: id, name, cover, description, author, publisher, price, storeNum, status, category_id
<== Row: 1, 别做傻瓜, bookcover/别做傻瓜.jpg, <<BLOB>>, 朱光潜, 北京出版社, 35.0, 0, 0, 6
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@307f6246]
2025-04-05 12:35:17.946 INFO 25860 --- [io-8111-exec-29] c.z.bean.common.DistributedLockAspect : 线程 http-nio-8111-exec-29 释放锁 [esbook:1]
...
测试结果成功,没有出现超卖