当前位置:   article > 正文

为什么要用分布式锁?分布式锁的实现?_数据库明明有锁,为什么还要分布式锁

数据库明明有锁,为什么还要分布式锁

分布式锁介绍

为什么要用锁?为什么要用分布式锁?
首先带着标题的两个问题,为什么要用锁?为什么要用分布式锁?

第一:为什么要用锁,很简单就一句话保证数据的安全性。在单机系统中,如果有多个线程同时操作同一个资源,就会出现数据安全问题。在Java中AtomicInteger、AtomicBoolean等java.util.concurrent包下的类能够保证单机系统的数据安全,同时还可以配合synchronized,lock使用。

第二:为什么要用分布式锁,因为现在大部分互联网系统都采用分布式部署,提升系统总体性能。在分布式系统中,一个方法在同一时间只能被一个机器的一个线程运行,在多个机器(也可以同一个机器起两个应用)中java.util.concurrent包和synchronized,lock就不能保证数据的安全性了,这时候就需要引入分布式锁。当然单机系统也可以使用分布式锁,但没有必要,会增加系统复杂度和降低系统可用性(中间件万一挂了-_-!)。

例如,12306购票系统,在面对高并发的情况下,首先采用限流,控制系统并发处理量,其次一张票只能出售给一位顾客,假如有n位顾客同时对同一张表下单,只会有一位顾客购买成功,这时候就需要通过锁来保证票只被第一位提交订单的客户购买。

分布式锁原理

简单画了一张图,服务A、B、C同时申请对资源A的使用权限,服务A申请成功,服务A可以使用资源A,服务B、C则等待A使用完或拒绝本次请求。

分布式锁实现思路

Redis实现分布式锁
通过资源的唯一id,生成一个key,缓存在redis中,在使用完资源后,移除该key,每次使用该资源前判断资源的key在redis中是否存在。
以上存在一个弊端,假如程序卡死或宕机,就不能释放锁了。这时候可以对key设置一个生效时间,需要评估好程序执行时间。
这时候还有一个弊端,假如时间设置过小了,有可能导致锁提前释放,也会引起数据安全性问题。这时候我们可以设置一个watchdog线程,在程序执行时对资源动态调整key生效时间。
这时候同样会有问题,程序卡死会无限续时的问题,这时候就需要根据具体业务去分析如何进行取舍。

Zookeeper实现分布式锁
zookeeper简称zk,zk是通过生成临时有序节点来实现分布式锁的,首先会在/lock目录下一个临时有序节点,后续请求会在节点后面继续创建临时节点。新的子节点后面,会添加一个次序编号,这个生成的编号,会在上一次的编号进行 +1 操作。

zk节点监听机制:每个线程抢占锁之前,先尝试创建自己的ZNode。同样,释放锁的时候,就需要删除创建的Znode。创建成功后,如果不是排号最小的节点,就处于等待通知的状态。等谁的通知呢?不需要其他人,只需要等前一个Znode的通知就可以了。前一个Znode删除的时候,会触发Znode事件,当前节点能监听到删除事件,就是轮到了自己占有锁的时候。第一个通知第二个、第二个通知第三个依次向后。

zk临时节点自动删除:当我们客户端断开连接之后,我们出创建的临时节点会进行自动删除操作,所以我们在使用分布式锁的时候,一般都是会去创建临时节点,这样可以避免因为网络异常等原因,造成的死锁。

分布式锁案例
本次案例使用springboot + redisson实现

  1. /**
  2. * 锁类型
  3. *
  4. * @Author h-bingo
  5. * @Date 2023-04-25 10:20
  6. * @Version 1.0
  7. */
  8. public enum LockType implements DescEnum {
  9. /**
  10. * 有一个线程加锁,其他线程直接结束
  11. * <p>
  12. * 此时需注意评估好程序执行时间,若锁持有时间小于程序执行时间,则会提前释放锁,可能引起锁失效问题
  13. */
  14. MUTEX("互斥锁"),
  15. /**
  16. * 有一个线程加锁,则等待一定时间
  17. * <p>
  18. * 此时需注意评估好程序执行时间,若锁持有时间小于程序执行时间,则会提前释放锁,可能引起锁失效问题
  19. */
  20. SYNC("同步锁"),
  21. /**
  22. * 有一个线程加锁,其他线程直接结束
  23. * <p>
  24. * 无需设置锁持有时间,需注意评估好程序不会一直卡死的问题,否则会出现锁无法释放,慎用
  25. */
  26. AUTO_RENEWAL_MUTEX("互斥锁(自动续期)"),
  27. /**
  28. * 有一个线程加锁,则等待一定时间
  29. * <p>
  30. * 无需设置锁持有时间,需注意评估好程序不会一直卡死的问题,否则会出现锁无法释放,慎用
  31. */
  32. AUTO_RENEWAL_SYNC("同步锁(自动续期)"),
  33. ;
  34. private String desc;
  35. LockType(String desc) {
  36. this.desc = desc;
  37. }
  38. @Override
  39. public String getDesc() {
  40. return desc;
  41. }
  42. }

 

  1. /**
  2. * RedisLockAspect 实现
  3. *
  4. * @Author h-bingo
  5. * @Date 2023-04-24 17:37
  6. * @Version 1.0
  7. */
  8. @Slf4j
  9. @Aspect
  10. @ConditionalOnMissingBean(RedisLockAspect.class)
  11. public class RedisLockAspect implements InitializingBean {
  12. private static final String REDIS_KEY_PREFIX = "redisLock:";
  13. @Autowired
  14. private RedissonClient redissonClient;
  15. @Pointcut("@annotation(com.bingo.study.common.component.lock.annotation.RedisLock)")
  16. public void redisLock() {
  17. }
  18. @Around(value = "redisLock()&&@annotation(lock)")
  19. public Object doAround(ProceedingJoinPoint joinPoint, RedisLock lock) throws Throwable {
  20. Object lockId = checkParam(joinPoint, lock);
  21. String lockKey = getLockKey(joinPoint, lockId, lock);
  22. if (lock.lockType() == LockType.MUTEX) {
  23. return doLock(joinPoint, 0, lock.leaseTime(), lockKey, joinPoint::proceed);
  24. } else if (lock.lockType() == LockType.AUTO_RENEWAL_MUTEX) {
  25. return doLock(joinPoint, 0, -1, lockKey, joinPoint::proceed);
  26. } else if (lock.lockType() == LockType.SYNC) {
  27. return doLock(joinPoint, lock.waitTime(), lock.leaseTime(), lockKey, joinPoint::proceed);
  28. } else if (lock.lockType() == LockType.AUTO_RENEWAL_SYNC) {
  29. return doLock(joinPoint, lock.waitTime(), -1, lockKey, joinPoint::proceed);
  30. }
  31. String methodName = AspectUtil.getMethodIntactName(joinPoint);
  32. log.warn("RedisLock锁类型异常[{}]", methodName);
  33. throw new RedisLockException(String.format("RedisLock锁类型异常[%s]", methodName));
  34. }
  35. private Object doLock(ProceedingJoinPoint joinPoint, long waitTime, long leaseTime, String lockKey,
  36. RedisLockCallBack callBack) throws Throwable {
  37. log.info("RedisLock Key: {}", lockKey);
  38. RLock rLock = redissonClient.getLock(lockKey);
  39. try {
  40. boolean tryLock = rLock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
  41. if (tryLock) {
  42. // 执行方法
  43. return callBack.doWork();
  44. } else {
  45. String methodName = AspectUtil.getMethodIntactName(joinPoint);
  46. log.info("RedisLock获取锁失败[{}]", methodName);
  47. throw new RedisLockException(String.format("RedisLock获取锁失败[%s]", methodName));
  48. }
  49. } finally {
  50. unLock(rLock);
  51. }
  52. }
  53. private void unLock(RLock rLock) {
  54. if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {
  55. rLock.unlock();
  56. }
  57. }
  58. /**
  59. * 若hasId为true,必须要有一个唯一的参数作为加锁key的一部分,并且这个参数要用 {@link LockId} 注解标识
  60. *
  61. * @Param [joinPoint, lock]
  62. * @Return void
  63. * @Date 2023-04-25 11:02
  64. */
  65. private Object checkParam(ProceedingJoinPoint joinPoint, RedisLock lock) {
  66. if (lock.hasId()) {
  67. Object[] args = joinPoint.getArgs();
  68. MethodSignature ms = (MethodSignature) joinPoint.getSignature();
  69. Method method = ms.getMethod();
  70. Parameter[] parameters = method.getParameters();
  71. if (parameters != null && parameters.length > 0) {
  72. for (int i = 0; i < parameters.length; i++) {
  73. LockId annotation = parameters[i].getAnnotation(LockId.class);
  74. if (annotation != null) {
  75. return args[i];
  76. }
  77. }
  78. }
  79. String methodName = AspectUtil.getMethodIntactName(joinPoint);
  80. log.error("缺少 @LockId 标识的唯一参数,method = {},args = {}", methodName, Arrays.toString(args));
  81. throw new RedisLockException("缺少 @LockId 标识的唯一参数");
  82. }
  83. return null;
  84. }
  85. /**
  86. * 如果 {@link RedisLock#hasId()} 为false
  87. * key 组成 applicationName + {@link RedisLockAspect#REDIS_KEY_PREFIX} + {@link RedisLock#key()}
  88. * <p>
  89. * 如果 {@link RedisLock#hasId()} 为true
  90. * key 组成 applicationName + {@link RedisLockAspect#REDIS_KEY_PREFIX} + {@link RedisLock#key()} + 方法 @LockId 参数
  91. * <p>
  92. * {@link RedisLock#key()} 为空则用方法名取代
  93. *
  94. * @Param [joinPoint, lockId, lock]
  95. * @Return java.lang.String
  96. * @Date 2023-04-25 10:54
  97. */
  98. private String getLockKey(ProceedingJoinPoint joinPoint, Object lockId, RedisLock lock) throws NoSuchMethodException {
  99. StringBuilder key = new StringBuilder(REDIS_KEY_PREFIX);
  100. if (StringUtils.isBlank(lock.key())) {
  101. key.append(AspectUtil.getMethodIntactName(joinPoint));
  102. } else {
  103. key.append(lock.key());
  104. }
  105. if (lock.hasId()) {
  106. key.append(":").append(lockId);
  107. }
  108. return RedisKeyUtil.getCacheKey(key.toString(), false, true);
  109. }
  110. @Override
  111. public void afterPropertiesSet() throws Exception {
  112. log.info("Redis分布式锁功能已开启,请在加锁方法添加: @RedisLock");
  113. }
  114. }
  1. /**
  2. * 标记参数为 lockKey 的一部分
  3. *
  4. * @Author h-bingo
  5. * @Date 2023-06-08 14:40
  6. * @Version 1.0
  7. */
  8. @Documented
  9. @Retention(value = RetentionPolicy.RUNTIME)
  10. @Target(value = {ElementType.PARAMETER})
  11. public @interface LockId {
  12. }
  1. /**
  2. * 开启 {@link RedisLock} 注解功能
  3. *
  4. * @Author h-bingo
  5. * @Date 2023-04-24 17:39
  6. * @Version 1.0
  7. */
  8. @Target(ElementType.TYPE)
  9. @Retention(RetentionPolicy.RUNTIME)
  10. @Documented
  11. @Import({RedisLockAspect.class})
  12. public @interface EnableRedisLock {
  13. }

测试结果

接口如下:这里设置睡眠 3 秒

可以看到测试日志,有 4 个获取锁失败,只有一个成功执行

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/745204
推荐阅读
相关标签
  

闽ICP备14008679号