• Redis分布式锁


    目录

     Redis分布式锁实现:

     1.首先是setnx命令实现分布式锁

     2.问题之设置过期时间防止别人拿不到锁)

    3.问题之设置uuid防止误删现象发生

    4.问题之最后判断+删除这里没有保证原子性: 

    5.问题之lock功能可重入的实现+重写加锁流程

     6.lua脚本重写解锁功能

    7.自定义分布式锁的构造+uuid识别

    所有代码 


    上锁lock()和解锁unlock()和ReentrantLock思路一致,逻辑给到我们自定义的DistributeRedisLock实现上锁和解锁逻辑

    1. public class ReentrantLock implements Lock, java.io.Serializable {
    2. private static final long serialVersionUID = 7373984872572414699L;
    3. /**提供所有实现机制的同步器
    4. private final Sync sync;
    5. /**
    6. * 此锁的同步控制基础。下面分为公平和非公平版本。使用 AQS 状态来表示持有锁的次数。
    7. */
    8. abstract static class Sync extends AbstractQueuedSynchronizer {
    9. private static final long serialVersionUID = -5179523762034025860L;

     Redis分布式锁实现:

    (66条消息) redis通过lua脚本实现分布式锁_Maxiao1204的博客-CSDN博客_lua实现redis分布式锁

    (66条消息) aqs的学习_Fairy要carry的博客-CSDN博客

     1.首先是setnx命令实现分布式锁

    加锁:其实就是cas操作,只有当key不存在的情况,才能将key的值设置为value,如果key已经存在了,那么setnx命令不做任何操作(Set if Not Exists)

    setnx key value
    

    那么setnx怎么释放锁呢?

     直接del key就ok了

    del key

     2.问题之设置过期时间防止别人拿不到锁)

    1.比如我们加锁后,如果忘记del锁的话,那么其他线程就一直无法获取到这个锁,造成一线程资源空转——>解决:上过期时间即可,所以说我们常说的redis分布式锁最基本的就是setnx+expire

    set lock 746879613 nx ex 20
    

    这里的nx和xx的区别

    (35条消息) Redis - NX与XX_平_繁的博客-CSDN博客_redis xx

    nx:如果不存在就会设置该值

    xx:如果存在就会覆盖该值 

    3.问题之设置uuid防止误删现象发生

    首先看redis分布式锁的流程

    1.加锁设置过期时间(一条命令保证原子性

    2.进行业务处理

    3.然后del锁

    场景:当A服务拿到了锁然后执行业务,业务时间很长>默认释放锁的时间,那么此时就会B拿到锁,然后再A业务执行完业务执行删除del时,会发现删的就是B服务的锁了

    ——>解决:加锁的时候除了设置过期时间并且设置UUID,然后删除的时候进行验证是不是该服务的,我们利用setIfAbsent(lockName,uuid,3,TimeUnit.SECONDS)设置lock的名称以及值也就是uuid作为标识——>(后面的可重入实现就得用lua脚本了,毕竟多了一个重复次数值)

    1. /**
    2. * 5.redis分布式锁循环实现(cas)
    3. */
    4. public void deduct5() {
    5. String uuid = UUID.randomUUID().toString();//每个请求自己的锁标识
    6. /**
    7. * 1.加锁setnx,并且设置过期时间-尽量保证原子性(不存在这个key就进行设置)
    8. */
    9. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
    10. /**
    11. * 2.判断是否获取锁成功
    12. */
    13. while (!lock) {//没获取到锁进行sleep
    14. try {
    15. Thread.sleep(50);
    16. // //2.1不成功进行cas操作——>此时递归操作可能会造成多次扣减库存的操作——>重复几次就扣减几次
    17. // this.deduct5();
    18. } catch (InterruptedException e) {
    19. e.printStackTrace();
    20. }
    21. }
    22. /**
    23. * 3.成功的执行操作
    24. */
    25. try {
    26. //1.查询库存信息
    27. Integer stock = (Integer) redisTemplate.opsForValue().get("stock");
    28. //2.判断库存是否充足
    29. if (stock != null && stock > 0) {
    30. redisTemplate.opsForValue().set("stock", stock - 1);
    31. }
    32. } finally {
    33. /**
    34. * 3.解锁需要判断是否是自己的锁
    35. * 注意:我们需要保证原子性,因为可能出现请求1执行然后3s后释放lock
    36. * ——>请求2进来的时候获得了自己的uuid并且拿到了锁
    37. * ——>请求1还没走完刚好到了finally这里,判断锁的值和uuid相等把请求2的锁删了——>需要保证原子性
    38. */
    39. if(StringUtils.equals(this.redisTemplate.opsForValue().get("lock").toString(),uuid)){
    40. /**
    41. * 4.进行解锁,将锁释放(可能出现误删现象)
    42. * 误删:比如我们第一个请求3s后自动释放锁资源,然后被请求2拿到,但是此时请求1的执行流程还没走完
    43. * ——>当请求1在finally的时候会删除lock锁资源,删除请求2的造成误删现象
    44. */
    45. this.redisTemplate.delete("lock");
    46. }
    47. }

    4.问题之最后判断+删除这里没有保证原子性: 

    当服务A业务执行完判断——>lock的值是否和当前事务的uuid值一样——>然后进行del,当判断完还没del时,加入锁过期了,事务B拿到锁资源,然后事务A之前就判断完了一删,B的锁就会被误删(没有保证判断和删除的原子性)

    解决:用lua脚本

    1. /**
    2. * 5.redis分布式锁循环实现(cas)
    3. */
    4. public void deduct5() {
    5. String uuid = UUID.randomUUID().toString();//每个请求自己的锁标识
    6. /**
    7. * 1.加锁setnx,并且设置过期时间-尽量保证原子性(不存在这个key就进行设置)
    8. */
    9. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
    10. /**
    11. * 2.判断是否获取锁成功
    12. */
    13. while (!lock) {//没获取到锁进行sleep
    14. try {
    15. Thread.sleep(50);
    16. // //2.1不成功进行cas操作——>此时递归操作可能会造成多次扣减库存的操作——>重复几次就扣减几次
    17. // this.deduct5();
    18. } catch (InterruptedException e) {
    19. e.printStackTrace();
    20. }
    21. }
    22. /**
    23. * 3.成功的执行操作
    24. */
    25. try {
    26. //1.查询库存信息
    27. Integer stock = (Integer) redisTemplate.opsForValue().get("stock");
    28. //2.判断库存是否充足
    29. if (stock != null && stock > 0) {
    30. redisTemplate.opsForValue().set("stock", stock - 1);
    31. }
    32. } finally {
    33. String script="if redis.call('get',KEYS[1]) == ARGV[1] "+
    34. "then "+
    35. " return redis.call('del','KEYS[1]')"+
    36. "else"+
    37. " return 0 "+
    38. "end";
    39. this.redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList("lock"),uuid);
    40. }
    41. }

    5.问题之lock功能可重入的实现+重写加锁流程

    原来的加锁方式是:setIfAbsent()——>这样加锁,lock只有一个值,就是uuid,作为我们线程的标识,那么我们如果可重入的话就需要再来个值放在uuid下面

    解决:我们可以利用hset——>设置锁名称为testlock,拥有标识为wyh,重入次数为1

    hset testlock wyh 1

    也可以利用incr,更推荐方便lua脚本编写,既可以作为增加重入次数,又可以作为创建新的锁

    加锁核心代码

    1.我们自定义一个分布式锁,然后实现Lock接口(类似ReentranLock,实现lock接口,并且aqs帮助了一个思路,但是具体的逻辑还是ReentrantLock自己实现)

    2.这里主要看我们的lua逻辑——>首先判断是否有这个锁,没有就直接创建一个锁并且设置标识以及当前重入次数1——>如果当前有锁并且是自己本身,也是一样利用incr增加重入次数——>否则返回0(注:参数lockName,uuid,过期时间u)

    3.然后redisTemplate执行我们这个lua脚本,如果执行失败就进行cas——>sleep,获取锁成功返回true锁自动续期问题后面说——>当前线程执行业务的时间太长>锁释放时间,导致锁提前释放——>导致B事务拿到锁后,A事务执行业务完后删不掉锁(需要比较uuid标识),确实也不需要删,只是这样隔离性较差)

    1. /**
    2. * 2.核心加锁方法
    3. *
    4. * @param time
    5. * @param unit
    6. * @return
    7. * @throws InterruptedException
    8. */
    9. @Override
    10. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    11. if (time != -1) {
    12. //2.1指定了时间设置为你指定的
    13. this.expire = unit.toSeconds(time);
    14. }
    15. /**
    16. * 2.2判断锁是否存在,判断对应锁的标识,也就是uuid下的拥有者是否存在
    17. * 如果不存在(也就是为0)的话就加锁值为1(代表第一次加锁),并且设置过期时间——>返回1,否则返回0
    18. */
    19. String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
    20. "then " +
    21. " redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
    22. " redis.call('expire', KEYS[1], ARGV[2]) " +
    23. " return 1 " +
    24. "else " +
    25. " return 0 " +
    26. "end";
    27. while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
    28. //2.3执行失败,循环尝试获取锁
    29. Thread.sleep(50);
    30. }
    31. /**
    32. * 2.4获取锁成功
    33. * 先获取自动续期
    34. */
    35. this.renewExpire();
    36. return true;
    37. }

     6.lua脚本重写解锁功能

    好处:前面都有说保证了我们加锁解锁的原子性,并且包含了各种可能的逻辑 

    1.判断是否有锁(并且符合标识,用hexists),比如wyh这个锁,标识为746879613的是否存在——>2.不存在返回nil——>3.如果存在进行解锁,hincrby将锁注入次数-1——>4.否则解锁失败返回0——>5.进行执行,如果返回为nil,说明解不了当前锁

    然后我们锁标识uuid是通过线程+服务结合而来的 

    HEXISTS wyh 746879613
    1. /**
    2. * 3.解锁方法
    3. */
    4. @Override
    5. public void unlock() {
    6. /**
    7. * 3.1判断线程是否拥有该锁(用uuid来判断)——>HEXISTS lock 746879613
    8. * ——>如果不存在说明解不了return nil——>如果解的了就-1,若-为0就删除
    9. */
    10. String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
    11. "then " +
    12. " return nil " +
    13. "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
    14. " then return redis.call('del', KEYS[1]) " +
    15. "else " +
    16. " return 0 " +
    17. "end";
    18. Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
    19. //判断结果异常
    20. if (flag == null) {
    21. throw new IllegalMonitorStateException("锁不属于你");
    22. }
    23. }

    7.自定义分布式锁的构造+uuid识别

     思路:1.我们的锁名称,redisTemplate这些由我们自定义的一个工厂类传入,并且设置过期时间——>2.uuid:由我们的工厂类(传入第一个标识(服务的)),与当前线程的uuid结合起来

    工厂类:

    1. package com.wyh.fbs_demo.com.wyh.lock;
    2. import org.springframework.beans.factory.annotation.Autowired;
    3. import org.springframework.data.redis.core.StringRedisTemplate;
    4. import org.springframework.stereotype.Component;
    5. import java.util.UUID;
    6. /**
    7. * 工厂类:提供redis的mysql的分布式锁
    8. */
    9. @Component
    10. public class DistributedLockClient {
    11. @Autowired
    12. private StringRedisTemplate redisTemplate;
    13. //不同服务的uuid
    14. private String uuid;
    15. public DistributedLockClient() {
    16. this.uuid= UUID.randomUUID().toString();
    17. }
    18. /**
    19. * 1.返回redis分布式锁,并且指定锁名称
    20. * @param name:锁名称
    21. * @return
    22. */
    23. public DistributeRedisLock getRedisLock(String name){
    24. return new DistributeRedisLock(redisTemplate,name,uuid);
    25. }
    26. }

    锁构造: 

    1. /**
    2. * Redis分布式锁没有被容器管理
    3. * 所以切记不能Autowired
    4. */
    5. public class DistributeRedisLock implements Lock {
    6. private StringRedisTemplate redisTemplate;
    7. public String lockName;//锁的名称
    8. private String uuid;//拥有锁的标识
    9. private long expire = 30;//默认过期时间
    10. /**
    11. * 1.构造redis分布式锁
    12. *
    13. * @param redisTemplate
    14. * @param lockName:锁名称
    15. * @param uuid:每个服务的uuid标识
    16. */
    17. public DistributeRedisLock(StringRedisTemplate redisTemplate, String lockName, String uuid) {
    18. this.redisTemplate = redisTemplate;
    19. this.lockName = lockName;
    20. this.uuid = uuid + ":" + Thread.currentThread().getId();//每个服务的id标识
    21. }

    所有代码 

    1. package com.wyh.fbs_demo.com.wyh.lock;
    2. import org.springframework.data.redis.core.StringRedisTemplate;
    3. import org.springframework.data.redis.core.script.DefaultRedisScript;
    4. import java.util.Arrays;
    5. import java.util.Timer;
    6. import java.util.TimerTask;
    7. import java.util.UUID;
    8. import java.util.concurrent.TimeUnit;
    9. import java.util.concurrent.locks.Condition;
    10. import java.util.concurrent.locks.Lock;
    11. /**
    12. * Redis分布式锁没有被容器管理
    13. * 所以切记不能Autowired
    14. */
    15. public class DistributeRedisLock implements Lock {
    16. private StringRedisTemplate redisTemplate;
    17. public String lockName;//锁的名称
    18. private String uuid;//拥有锁的标识
    19. private long expire = 30;//默认过期时间
    20. /**
    21. * 1.构造redis分布式锁
    22. *
    23. * @param redisTemplate
    24. * @param lockName:锁名称
    25. * @param uuid:每个服务的uuid标识
    26. */
    27. public DistributeRedisLock(StringRedisTemplate redisTemplate, String lockName, String uuid) {
    28. this.redisTemplate = redisTemplate;
    29. this.lockName = lockName;
    30. this.uuid = uuid + ":" + Thread.currentThread().getId();//每个服务的id标识
    31. }
    32. @Override
    33. public void lock() {
    34. //1.尝试获取锁
    35. this.tryLock();
    36. }
    37. @Override
    38. public void lockInterruptibly() throws InterruptedException {
    39. }
    40. @Override
    41. public boolean tryLock() {
    42. try {
    43. //1.加锁核心,设置默认的过期时间
    44. return this.tryLock(-1L, TimeUnit.SECONDS);
    45. } catch (InterruptedException e) {
    46. e.printStackTrace();
    47. }
    48. return false;
    49. }
    50. /**
    51. * 2.核心加锁方法
    52. *
    53. * @param time
    54. * @param unit
    55. * @return
    56. * @throws InterruptedException
    57. */
    58. @Override
    59. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    60. if (time != -1) {
    61. //2.1指定了时间设置为你指定的
    62. this.expire = unit.toSeconds(time);
    63. }
    64. /**
    65. * 2.2判断锁是否存在,判断对应锁的标识,也就是uuid下的拥有者是否存在
    66. * 如果不存在(也就是为0)的话就加锁值为1(代表第一次加锁),并且设置过期时间——>返回1,否则返回0
    67. */
    68. String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
    69. "then " +
    70. " redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
    71. " redis.call('expire', KEYS[1], ARGV[2]) " +
    72. " return 1 " +
    73. "else " +
    74. " return 0 " +
    75. "end";
    76. while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
    77. //2.3执行失败,循环尝试获取锁
    78. Thread.sleep(50);
    79. }
    80. /**
    81. * 2.4获取锁成功
    82. * 先获取自动续期
    83. */
    84. this.renewExpire();
    85. return true;
    86. }
    87. /**
    88. * 3.解锁方法
    89. */
    90. @Override
    91. public void unlock() {
    92. /**
    93. * 3.1判断线程是否拥有该锁(用uuid来判断)——>HEXISTS lock 746879613
    94. * ——>如果不存在说明解不了return nil——>如果解的了就-1,若-为0就删除
    95. */
    96. String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
    97. "then " +
    98. " return nil " +
    99. "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
    100. " then return redis.call('del', KEYS[1]) " +
    101. "else " +
    102. " return 0 " +
    103. "end";
    104. Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
    105. //判断结果异常
    106. if (flag == null) {
    107. throw new IllegalMonitorStateException("锁不属于你");
    108. }
    109. }
    110. @Override
    111. public Condition newCondition() {
    112. return null;
    113. }
    114. /**
    115. * 4.获取当前线程的id,与服务id做拼接保证唯一性
    116. *
    117. * @return
    118. */
    119. // String getId() {
    120. // return uuid + ":" + Thread.currentThread().getId();
    121. // }
    122. /**
    123. * 5.定时器功能——>重置时间自动续期
    124. */
    125. private void renewExpire() {
    126. String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
    127. "then " +
    128. " return redis.call('expire',KEYS[1],ARGV[2]) " +
    129. "else " +
    130. " return 0 " +
    131. "end";
    132. new Timer().schedule(new TimerTask() {
    133. @Override
    134. public void run() {
    135. //这里注意子线程需要的uuid肯定得和父id一样,不然获得不了锁
    136. Boolean flag = redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire));
    137. if (flag) {
    138. renewExpire();
    139. }
    140. }
    141. }, this.expire * 1000 / 3, this.expire * 1000 / 3);
    142. }
    143. }


    测试:

    1. @Service
    2. //@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
    3. public class StockService {
    4. @Resource(name = "redisTemplate")
    5. private RedisTemplate redisTemplate;
    6. @Autowired
    7. private StockMapper stockMapper;
    8. //注入工厂类
    9. @Autowired
    10. private DistributedLockClient client;
    11. // @Autowired
    12. // private ReentrantLock lock = new ReentrantLock();
    13. /**
    14. * 6.redis分布式锁实现——可重入加锁减锁
    15. */
    16. public void deduct6(){
    17. /**
    18. * 1.获取redis分布式锁
    19. */
    20. DistributeRedisLock redisLock = this.client.getRedisLock("lock");
    21. redisLock.lock();
    22. try {
    23. /**
    24. * 2.对库存信息进行操作
    25. */
    26. String stock = redisTemplate.opsForValue().get("stock").toString();
    27. //2.1判断库存是否充足
    28. if(stock!=null&&stock.length()!=0){
    29. Integer st = Integer.valueOf(stock);
    30. //2.2将库存减-1
    31. if(st>0) redisTemplate.opsForValue().set("stock",String.valueOf(--st));
    32. }
    33. //2.3可重入测试
    34. this.test();
    35. } finally {
    36. /**
    37. * 3.解锁
    38. */
    39. redisLock.unlock();
    40. }
    41. }
    42. /**
    43. * 7.测试可重入
    44. */
    45. public void test(){
    46. DistributeRedisLock lock = this.client.getRedisLock("lock");
    47. lock.lock();
    48. System.out.println("测试可重入锁....");
    49. lock.unlock();
    50. }
    51. }

  • 相关阅读:
    【无标题】
    (附源码)spring boot大学英语智慧教学平台 毕业设计 201722
    340页11万字智慧政务大数据资源平台大数据底座数据治理建设方案
    技能 | 在项目中集成Sentry
    下载nvm管理node版本
    Java内存模型之happens-before
    当EAI遇见RPA:破解接口难题,助力数据打通
    学习gmock
    单元测试探析:什么是Stubs、Mocks、Spies、Dummies?带你了解4个核心工具
    微信小程序简单输入框界面(实现选择标签功能)
  • 原文地址:https://blog.csdn.net/weixin_57128596/article/details/126881970