关于分布式锁,我也在网上找了很多文章,但始终没有找到一个我想要的解决方案,于是参考前辈的思想和加入自己的想法
手写了一个。
什么是分布式锁:
就是在多个服务端都能访问到的中间件上打上一个标记。例如:redis, zookeeper, mysql
本文是基于redis
的 setnx
实现分布式锁,但是我写的分布式锁解决方案,还可以适配其他中间件实现。这个才是重点。
先说下分布式锁需要解决的问题:
- 有一个线程获取到了锁,其他线程是阻塞等待,还是直接以失败响应。
- 获取锁失败,是否需要重试一会。
- 锁必需要具备有可重入性质。
- 服务端获取锁后,业务异常或宕机了,没有释放锁,导致死锁如何处理。
- 获取锁了,但是业务执行过长,如何处理。(
锁续命
) - 锁失效后(
redis key过期
),业务还没有执行完,如何处理。 A服务端
释放了锁,B服务端
阻塞等待获取锁的线程,如何唤醒。线程A
获取的锁,线程B
不能释放。- 阻塞等待被唤醒获取锁的线程,超时如何处理。
- 锁是
公平
还是不公平
的。 - AOP 执行获取锁处理,异常后事务怎么处理。
- AOP 执行获取锁处理,获取失败后,如何返回错误信息,如何通知给开发者。
- AOP 执行获取锁处理,
它本身是否线程安全,这点非常重要
。
直接上效果图看看:
goods 表的 goodsId = 1 有 100个库存
业务操作数据库的代码
并发200
测试
不应用分布式锁效果:库存直接 负100
了。
应用分布式锁效果:库存始终不会超出限制。
进入正文,开始分享我设计的分布式锁的思路。自我感觉还是写得蛮好的。😂😂😂
应用的技术点:必须依赖Spring
设计原则:
- 依赖倒转(倒置)原则
- 开闭原则
- 迪米特法则
- 合成复用原则
设计模式:
- 单列模式
- 外观模式
- 工厂模式
- 模板方法模式
- 桥接模式
围绕以下设计,展开程序代码的编写
获取锁成功处理:
- 获取锁成功,需要开启一个续命线程,重置锁的过期时间。
- 当前线程是可重入的,重入次数加一。
- 如果没有应用Aop事务,则手动开启事务。
- 业务执行完毕,释放锁,重入次数减一,如果重入次数等于0,则删除redis key,并唤醒阻塞等待的线程。
- 如果没有应用Aop事务,则手动提交事务。
获取锁不成功处理:
- 进行重试获取锁,有次数限制,且每隔多少时间重试一次。
- 重试获取锁失败,如果需要阻塞等待,则阻塞当前线程,并加入到阻塞队列集合中,等待被唤醒。
- 开启一个定时任务 ,清理阻塞队列中,获取锁超时的线程。
被唤醒去获取锁的线程处理:
- 检查当前唤醒的线程,是否有超时。
获取锁失败或异常处理:
- 调用获取锁失败的回调接口方法。
- 如果获取锁失败的回调方法,返回了自定义的异常,则以该异常抛出。
- 否则以获取锁的异常抛出。
Redis分布式锁处理:
- 获取锁之前,订阅redis key删除事件监听。
- 获取锁成功,订阅redis key过期事件监听。
重点说明:
Aop分布式事务失败或者业务执行异常全部以异常抛出,所以需要全局捕获异常处理。
为什么要使用AOP
来实现分布式锁
既然是获取锁,才能执行业务逻辑。那就不应该让获取锁流程,参与到主业务中,强力实现解耦。
也使得代码阅读起来,通俗易懂。而且将获取锁,释放锁全部交给Aop。如果在主业务中获取锁,那还需要去关心他的释放锁。
@DistributedLock 使用分布式锁注解 演示:
/* 注解全部属性 */
@DistributedLock(lockKey = "goods::goodsId-${goodsId}", // 锁定的 key 支持表达式在方法参数中取值 必须指定
trySleep = 60, // 重试获取锁的间隔 这是默认值
tryRestrict = 5, // 重试获取锁的次数限制 这是默认值
resetRestrict = 3, // 续命次数限制 这是默认值
expire = 3000, // 锁的失效时间 单位毫秒 这是默认值
transaction = true, // 是否需要以事务执行 这是默认值
lockFailHandler = GoodsLockFailHandler.class, // 获取锁失败的回调, 该类型必须要注入到Spring容器中 默认无(没有回调)
lockProcessed = RedisLockProcessedImpl.class, // 锁的解决方案, 必须是一个由Spring容器创建的类型 这是默认值
await = @DistributedLock.Await( // 获取锁的阻塞等待策略
isAwait = false, // 是否需要阻塞, 获取锁失败后阻塞, 等待被唤醒. 这是默认值
timeout = 10000, // 等待超时时间 单位毫秒 从线程等待开始计算 这是默认值
fair = false, // 等待中的线程 唤醒是否公平 这是默认值
awaitRestrict = Integer.MAX_VALUE // 阻塞等待获取锁的队列 大小限制 这是默认值
),
filter = @DistributedLock.Filter( // 获取锁之前的过滤
GoodsLockFilter.class // 该类型必须要注入到Spring容器中 默认无(过滤)
)
)
/**
* 实际只需要以下属性即可
* 甚至获取锁失败的回调 lockFailHandler 和 获取锁之前的过滤 filter 都可以不指定
* 因为其他属性都有默认值
*/
@DistributedLock(lockKey = "goods::goodsId-${goodsId}", // 锁定的 key 支持表达式在方法参数中取值 必须指定
lockFailHandler = GoodsLockFailHandler.class, // 获取锁失败的回调, 该类型必须要注入到Spring容器中 默认无(没有回调)
filter = @DistributedLock.Filter( // 获取锁之前的过滤
GoodsLockFilter.class // 该类型必须要注入到Spring容器中 默认无(过滤)
)
)
使用案例演示:
1:必须全局捕获 LockException
异常,给前端响应信息。否则响应的是一堆错误。
@ControllerAdvice
@ResponseBody
public class ExceptionController {
/**
* 分布式锁异常 全局捕获
*
* @param e
* @return
*/
@ExceptionHandler(LockException.class)
public Result lockException(LockException e) {
return new Result(-1, e.getMessage(), null);
}
}
2:必须先启用分布式锁@EnableDistributedLock
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({LockRegister.class, LockImportRegister.class})
public @interface EnableDistributedLock {
/**
* 指定 分布式锁 的处理器
*/
Class<? extends LockProcessed> defLockProcessed() default LockProcessed.class;
}
我这里用的是 Redis 实现分布式锁,所以直接使用@EnableRedisLock这个注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
/* 启用分布式锁 */
@EnableDistributedLock(defLockProcessed = RedisLockProcessedImpl.class)
@Import({RedisLockRegister.class})
public @interface EnableRedisLock {
}
Demo01:
指定一个获取锁之前的过滤器,该过滤器需要注入到Spring容器中
// lockKey 支持表达式 goodsId = 1 那么 lockKey = goods::goodsId-1
@DistributedLock(lockKey = "goods::goodsId-${goodsId}",
// 指定获取锁之前的过滤
filter = @DistributedLock.Filter(GoodsLockFilter.class)
)
public boolean buy(Integer goodsId) {
return true;
}
GoodsLockFilter
@Component
public class GoodsLockFilter implements LockFilter {
/**
* 过期锁之前的过滤 是否予许获取锁
* @param methodParams 方法参数
*/
@Override
public boolean allow(Map<String, Object> methodParams) {
/* 不予许获取锁 false */
return false;
}
/**
* 不予许获取锁的提示信息
* @param methodParams 方法参数
* @return
*/
@Override
public String errorMessage(Map<String, Object> methodParams) {
return "没有库存了";
}
}
Postman 测试结果 http://localhost:8080/buy?goodsId=1
Demo02:
全局捕获异常添加一个自定义的异常
@ControllerAdvice
@ResponseBody
public class ExceptionController {
/**
* 自定义异常捕获
* @param e
* @return
*/
@ExceptionHandler(MyException.class)
public Result myException(MyException e) {
return new Result(-1, e.getMessage(), null);
}
}
分布式锁指定一个获取锁失败的回调
// lockKey 支持表达式 goodsId = 1 那么 lockKey = goods::goodsId-1
@DistributedLock(lockKey = "goods::goodsId-${goodsId}",
/* 指定一个获取锁失败的回调 */
lockFailHandler = GoodsLockFailHandler.class
)
public boolean buy(Integer goodsId, Integer userId) {
/* 故意抛出异常 */
int i = 1 / 0;
return true;
}
GoodsLockFailHandler
@Component
public class GoodsLockFailHandler implements LockFailHandler {
@Override
public LockFailResult handle(LockFailInfo lockFailInfo) throws Exception {
System.out.println("获取锁失败了 " + lockFailInfo);
return new LockFailResult(new MyException("获取锁失败了, 抛出自定义的异常 " + lockFailInfo));
/* 如果不想抛出自定义的异常, 返回 null 即可 */
//return LockFailResult.RESULT_EMPTY;
}
}
Postman 测试结果 http://localhost:8080/buy?goodsId=1
总结:
- 如果想在获取锁之前,检查一些数据,可以指定一个过滤器。
- 如果获取锁失败了(结束了),想记录一些信息,比如日志,可以指定一个回调。
- 如果想要让获取锁不成功的线程,阻塞等待,可以开启 阻塞策略。
过滤器和失败回调器 都是从Spring中获取,所以大可放心的应用Spring的功能。
此分布式锁功能面还是蛮多的,需要源码的可以私信我,或扫下面的码加我微信。
项目结构:
1:common.support.spring.aop
Aop基础。
2:common.support.spring.lock
分布式锁支持。
3:common.support.spring.redislock
Redis分布式锁实现。
以上目录结构划分,是为了区分各个模板,实现解耦,在项目移值的时候便捷。哪怕复制粘贴,都可以不用改代码,直接使用。
还有就是要应用到我们现成的开发框架中。此项目存在公共模块中。
分布式锁 项目结构:
1:annotation
注解相关。
- DistributedLock 使用分布式锁 的注解。
- DistributedLockAspect 分布式锁的Aop切面执行器。
- EnableDistributedLock 启用分布式锁的注解。
- LockData DistributedLock注解的属性包装。
- LockImportRegister 注册相关的类至Spring。
- LockRegister 注册相关的类至Spring。
2:exception
异常。分布式锁获取锁失败都是以异常抛出。
- LockAwaitRestrictException 阻塞队列超过限制异常。
- LockException 分布式锁的顶级异常,所有异常继承自他。
- LockFailException 获取锁失败异常。
- LockFilterException 在获取锁, 过滤时, 不予许获取锁的异常。
- LockProcessedException 解析/包装/注解信息异常。
- LockReentryRestrictException 重入次数达到限制异常。
- LockTimeoutException 获取锁超时异常。
- UnlockException 释放锁异常。
3:factory
生产对象实例工厂。
- LockFailHandlerFactory生产获取锁失败回调实例。
- LockFilterFactory 生产获取锁之前过滤实例。
- LockProcessedFactory 生产分布式锁处理器实例。
4:info
描述信息
- AwaitInfo 阻塞等待被唤醒获取锁的信息。
- LockFailInfo 获取锁失败的回调描述信息。
- LockFailResult 获取锁失败的回调的返回信息。
- LockInfo 获取锁的信息。
- LockResult 获取锁/释放锁的结果。
5:info
跟目录
- AbstractDistributedLockProcessed 分布式锁的处理器,已经提供好了算法骨架。
- LockFailHandler 获取锁失败的回调接口。
- LockFilter 获取锁之前的回调接口。
- LockProcessed 分布式锁处理接口。
- TransactionStatusWrapper 事务包装类。
总结:
如果想要更换 分布式锁的 实现, 只需要继承该AbstractDistributedLockProcessed抽象类,重写他的抽象方法,对获取锁/释放锁的细节完善即可,
然后在启用分布式锁@EnableRedisLock的属性defLockProcessed标记上。
或者在使用分布式锁注解DistributedLock属性lockProcessed标记也可以
符合开闭原则,完全不需要更改原有代码逻辑,拓展非常容易。
Redis实现分布式锁 项目结构:
1:annotation
注解相关。
- EnableRedisLock 启用Redis分布式锁。
- RedisLockRegister 向Spring容器注册相关类。
2:handle
redis key的监听
- AbstractKeyspaceEventMessageListener key 监听的抽象类。
- DeleteKeyspaceEventMessageListener key删除监听
- ExpiredKeyspaceEventMessageListener key 过期监听
- KeyListenerHandler key 监听回调接口
3:redislock
跟目录
- AbstractRedisDistributedLockProcessed Redsi 分布式锁的抽象类。
- RedisLockProcessedImpl Redsi 分布式锁的实现。
总结:
Redis实现分布式锁非常简单,只需要实现获取锁/释放锁/续命锁
的细节,因为其他的业务逻辑在父类中AbstractDistributedLockProcessed 已经完善好了。
RedisLockProcessedImpl源码
public class RedisLockProcessedImpl extends AbstractRedisDistributedLockProcessed {
@Autowired
protected StringRedisTemplate stringRedisTemplate;
public RedisLockProcessedImpl(LockData lockData) throws LockProcessedException {
super(lockData);
}
// 获取锁
@Override
protected boolean doLock() {
return stringRedisTemplate.opsForValue()
.setIfAbsent(getLockKey(), getLockValue(), getExpire(), TimeUnit.MILLISECONDS);
}
// 续命锁
@Override
protected void doReset() {
stringRedisTemplate.expire(getLockKey(), getExpire(), TimeUnit.MILLISECONDS);
}
// 释放锁
@Override
protected boolean doUnlock() {
return stringRedisTemplate.delete(getLockKey());
}
}
AbstractRedisDistributedLockProcessed源码
public abstract class AbstractRedisDistributedLockProcessed extends AbstractDistributedLockProcessed {
public static final String LOCK_KEY_PREFIX = "DISTRIBUTED-LOCK::";
@Autowired
private DeleteKeyspaceEventMessageListener delKeyListener;
@Autowired
private ExpiredKeyspaceEventMessageListener expiredKeyListener;
public AbstractRedisDistributedLockProcessed(LockData lockData) throws LockProcessedException {
super(lockData);
}
@Override
public String getLockKey() {
String lockKey = super.getLockKey();
return lockKey == null ? "" : LOCK_KEY_PREFIX + lockKey;
}
@Override
protected void prepareLock() {
/**
* 监听删除key的监听
* 从而可以让其他<code>服务端</code>监听到 锁定的key 被删除了,也就是(锁释放了),可以作唤醒等待获取线程操作
*/
if (!delKeyListener.exist(getLockKey())) {
delKeyListener.register(getLockKey(), key -> {
try {
/* 唤醒阻塞线程 */
wakeup();
} catch (Exception e) {
}
});
}
}
/**
* 获取锁成功的回调
*/
@Override
protected void doSuccess() {
/**
* 监听 锁定key 失效事件
*/
if (!expiredKeyListener.exist(getLockKey())) {
expiredKeyListener.register(getLockKey(), key -> {
try {
/* redis 锁的key过期了 但是服务端没有释放锁 */
if (isLock()) {
/* 以异常业务结束当前还获取锁的线程 */
unusualService();
}
/* 唤醒等待的线程 */
wakeup();
} catch (Exception e) {
}
});
}
}
}
觉得对您有帮助,就点个赞呗。😀
有需要源码的可以扫码加找我拿,或者私聊我。