【分布式锁】Redis实现分布式锁

打印 上一主题 下一主题

主题 868|帖子 868|积分 2604

在分布式系统中,当多个服务实例(或节点)需要访问或修改同一份共享资源时,就需要使用分布式锁来确保数据的一致性和防止并发问题。这种情况下,传统的Java并发控制机制如ReentrantLock或synchronized就无法满足需求,因为它们是JVM级别的锁,只能保证单个JVM实例内部的线程同步,而无法跨JVM实例举行同步。
  一、一些需要使用分布式锁而无法使用ReentrantLock或synchronized的场景:



  • 跨JVM的资源共享:在分布式系统中,服务通常被部署在多个JVM实例上,这些实例可能分布在差别的物理呆板或容器上。当这些实例需要访问或修改同一个数据库记载、缓存项或任何形式的共享资源时,就需要使用分布式锁来确保数据的一致性和操作的原子性。
  • 微服务架构中的分布式事件:在微服务架构中,服务之间通常通过轻量级的通讯协议(如HTTP REST API)举行交互。由于服务是独立部署和运行的,因此它们之间的数据一致性需要额外的机制来保证。在某些情况下,可能需要在多个服务之间和谐事件操作,这时就需要使用分布式锁来确保事件的完整性和一致性。
  • 分布式缓存的一致性:在使用分布式缓存(如Redis、Memcached等)时,多个服务实例可能会同时读取或写入同一个缓存项。为了确保缓存数据的一致性和减少缓存击穿、雪崩等问题的发生,可能需要使用分布式锁来控制对缓存的并发访问。
  • 分布式文件系统的同步:在分布式文件系统中,多个节点可能同时需要访问或修改同一个文件或目录。为了防止数据冲突和保证文件系统的一致性,需要使用分布式锁来和谐各个节点之间的访问操作。
  • 定时任务或作业的分发:在分布式系统中,可能需要运行一些定时任务或批处置惩罚作业。为了防止多个节点同时实行同一个任务而导致数据重复处置惩罚或资源争用,可以使用分布式锁来确保任务的分发和实行是唯一的。

分布式锁提供了一种跨JVM实例的同步机制,可以确保在分布式环境中对共享资源的访问是安全和一致的。实现分布式锁有多种方式,包罗使用数据库、Redis、Zookeeper等中心件来提供锁服务。选择符合的分布式锁实现取决于具体的应用场景、性能要求和系统架构。
接下来我们通过Redis来实现分布式锁。
在实现分布式锁之前,我们需要考虑一个问题,分布式锁是对共享资源或者数据的安全保护,确保数据的一致性和防止并发问题。那同时间内对某个接口的并发请求,怎么模拟呢 ?我这里接纳的Nginx来实现。
二、Nginx实现反向代理、负载均衡


1.官网上下载nginx

具体的安装过程和全部配置 可见博客:【nginx】nginx的配置文件到底是什么布局,到底怎么写?
2.配置负载均衡。


3.具体配置如下:


4.主要配置如下:

  1. http {
  2.         upstream backend {
  3.                 server 127.0.0.1:8023;
  4.                 server 127.0.0.1:8021;
  5.                 }
  6.     include       mime.types;
  7.     default_type  application/octet-stream;
  8.     #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
  9.     #                  '$status $body_bytes_sent "$http_referer" '
  10.     #                  '"$http_user_agent" "$http_x_forwarded_for"';
  11.     #access_log  logs/access.log  main;
  12.     sendfile        on;
  13.     #tcp_nopush     on;
  14.     #keepalive_timeout  0;
  15.     keepalive_timeout  65;
  16.     #gzip  on;
  17.     server {
  18.         listen       80;
  19.         server_name  localhost;
  20.                 location / {
  21.                         proxy_pass http://backend;
  22.                         proxy_set_header Host $host;
  23.                         proxy_set_header X-Real-IP $remote_addr;
  24.                         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ##反向代理执行定义的upstream名字
  25.                 }
  26.         #charset koi8-r;
  27. ...................
复制代码
三、Redis实现分布式锁

场景:
我分别在cloud项目的差别模块,誊写了可以处置惩罚这个请求的代码,模拟部署在差别的环境上。以上的Nginx配置将会以轮训的方式举行服务调用。
布局如下:
模块1:provider-and-consumer ,端口:8023

模块2: rabbitmq-consumer 端口8021

模块1: providerconsumer 端口 8023
RedisTestController.java如下:
  1. package com.atguigu.gulimall.providerconsumer.controller;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.redisson.api.RLock;
  4. import org.redisson.api.RedissonClient;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.data.redis.core.StringRedisTemplate;
  7. import org.springframework.web.bind.annotation.GetMapping;
  8. import org.springframework.web.bind.annotation.RequestMapping;
  9. import org.springframework.web.bind.annotation.RestController;
  10. import java.util.UUID;
  11. import java.util.concurrent.TimeUnit;
  12. /**
  13. * @author: jd
  14. * @create: 2024-07-08
  15. */
  16. @RestController
  17. @RequestMapping("/test")
  18. @Slf4j
  19. public class RedisTestController {
  20.     @Autowired
  21.     private StringRedisTemplate stringRedisTemplate;
  22.     /**
  23.      * 分布式锁使用
  24.      */
  25.     @GetMapping("/lock")
  26.     public  String deductStock() {
  27.         //获取一个固定的商品ID,作为我们被秒杀的商品ID  这个代表是一种商品,加锁的时候对这个商品加锁,
  28.         // (接着上面)只有持有这个商品redis锁的,才能对这个商品的库存进行扣减,否则加锁不成功的,代表当前的商品的库存正在被另外一个请求进行扣减,当前的这个扣减失败。需要重新下单
  29.         String lockKey="lock:product:101";
  30.         //uuid,防止删除其他人加的锁
  31.         String clientId = UUID.randomUUID().toString();
  32.         //进行加锁,设置过期时间为10s 注意代码的原子性,虽然设置了锁的过期时间是10s,但是仍然存在一个问题,如果我业务还没执行完,锁失效了怎么办 ?此时正在执行的业务中比如进行扣减库存等,会导致重复扣减,超卖等问题。
  33.         Boolean result =stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10, TimeUnit.SECONDS);
  34.         //如果加锁失败,返回错误,秒未成功
  35.         if(result){
  36.             System.out.println("=====>8023  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值= " + stringRedisTemplate.opsForValue().get(lockKey)+"时间:"+System.currentTimeMillis()+" 即将扣减库存");
  37.         }else {
  38.             System.out.println("=====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 ");
  39.             return "";
  40.         }
  41. //        if(!result){
  42. //            return "error_code";
  43. //        }
  44.         try {
  45.             // 获取当前库存
  46.             String stock1 = stringRedisTemplate.opsForValue().get("stock");
  47.             if (stock1 == null) {
  48.                 System.out.println("秒杀未开始");
  49.                 return "end";  //如果遇到这种直接返回的情况,最初也加上锁了,在最后的finally中也会释放锁,所以不会产生死锁。导致无法扣减库存
  50.             }
  51.             int stock = Integer.parseInt(stock1);
  52.             System.out.println("====>拿到商品库存,库存数量 = " + stock);
  53.             if (stock > 0) {
  54.                 // 扣减库存
  55.                 int realStock = stock - 1;
  56.                 // 更新库存
  57.                 Thread.sleep(1); //为了不去掉下面的InterruptedException 这个捕捉,所以直接睡了1毫秒
  58.                 System.out.println("*****线程睡眠,模仿业务花费的时间1毫秒");
  59.                 stringRedisTemplate.opsForValue().set("stock", realStock + ""); //扣减完库存重新的设置上当前处理商品的现有库存数量。
  60.                 System.out.println("扣减成功,剩余的库存为:" + realStock);
  61.             } else {
  62.                 System.out.println("扣减失败,库存不足");
  63.             }
  64.         } catch (InterruptedException e) {
  65.             e.printStackTrace();
  66.         } finally {
  67.             //如果是自己加的锁就自己删掉,防止死锁
  68.             if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
  69.                 System.out.println("=====>8023释放锁成功" );
  70.                 stringRedisTemplate.delete(lockKey);
  71.             }
  72.         }
  73.         return "end";
  74.     }
  75.     }
复制代码
模块2代码:
RedisTestController.java代码
  1. package com.atguigu.gulimall.rabbitmqconsumer.controller;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.redisson.api.RLock;
  4. import org.redisson.api.RedissonClient;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.data.redis.core.StringRedisTemplate;
  7. import org.springframework.web.bind.annotation.GetMapping;
  8. import org.springframework.web.bind.annotation.RequestMapping;
  9. import org.springframework.web.bind.annotation.RestController;
  10. import java.util.UUID;
  11. import java.util.concurrent.TimeUnit;
  12. /**
  13. *
  14. * 和provider-and-consumer 这两个服务中都有这个RedisTestController,用来模拟两个不同的服务
  15. * @author: jd
  16. * @create: 2024-07-08
  17. */
  18. @RestController
  19. @RequestMapping("/test")
  20. @Slf4j
  21. public class RedisTestController {
  22.     @Autowired
  23.     private StringRedisTemplate stringRedisTemplate;
  24.     /**
  25.      * 分布式锁使用
  26.      */
  27.     @GetMapping("/lock")
  28.     public  String deductStock() {
  29.         //获取一个固定的商品ID,作为我们被秒杀的商品ID  这个代表是一种商品,加锁的时候对这个商品加锁,
  30.         // (接着上面)只有持有这个商品redis锁的,才能对这个商品的库存进行扣减,否则加锁不成功的,代表当前的商品的库存正在被另外一个请求进行扣减,当前的这个扣减失败。需要重新下单
  31.         String lockKey="lock:product:101";
  32.         //uuid,防止删除其他人加的锁
  33.         String clientId = UUID.randomUUID().toString(); //锁的value值是随机的。
  34.         //进行加锁,设置过期时间为10s 注意代码的原子性(这个原子性是指,加上锁的动作和为这个锁设置过期时间的动作保证是一个原子),虽然设置了锁的过期时间是10s,但是仍然存在一个问题,如果我业务还没执行完,锁失效了怎么办 ?此时正在执行的业务中比如进行扣减库存等,会导致重复扣减,超卖等问题。
  35.         Boolean result =stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10, TimeUnit.SECONDS);
  36.         if(result){
  37.             System.out.println("=====>8021  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值=" + stringRedisTemplate.opsForValue().get(lockKey)+"时间:"+System.currentTimeMillis()+"  即将扣减库存");
  38.         }else {
  39.             System.out.println("=====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单 ");
  40.             return "";
  41.         }
  42.         /*//如果加锁失败,返回错误,秒未成功
  43.         if(!result){
  44.             return "error_code";
  45.         }*/
  46.         try {
  47.             // 获取当前库存
  48.             String stock1 = stringRedisTemplate.opsForValue().get("stock");
  49.             if (stock1 == null) {
  50.                 System.out.println("秒杀未开始");
  51.                 return "end";  //如果遇到这种直接返回的情况,最初也加上锁了,在最后的finally中也会释放锁,所以不会产生死锁。导致无法扣减库存
  52.             }
  53.             int stock = Integer.parseInt(stock1);
  54.             System.out.println("=====>拿到商品库存,库存数量 = " + stock);
  55.             if (stock > 0) {
  56.                 // 扣减库存
  57.                 int realStock = stock - 1;
  58.                 // 更新库存
  59.                 Thread.sleep(1); //1ms相当于没休眠。 为了不去掉下面的InterruptedException 这个捕捉,所以直接睡了1毫秒
  60.                 // Thread.sleep(20000); //这个是为了模拟 网络不好的情况下,花费的时间20s
  61.                 System.out.println("*****线程睡眠,模仿业务花费的时间1毫秒");
  62.                 stringRedisTemplate.opsForValue().set("stock", realStock + ""); //扣减完库存重新的设置上当前处理商品的现有库存数量。
  63.                 System.out.println("扣减成功,剩余的库存为:" + realStock);
  64.             } else {
  65.                 System.out.println("扣减失败,库存不足");
  66.             }
  67.         } catch (InterruptedException e) {
  68.             e.printStackTrace();
  69.         } finally {
  70.             //如果是自己加的锁就自己删掉,防止死锁
  71.             if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
  72.                 System.out.println("======>8021释放锁成功" );
  73.                 stringRedisTemplate.delete(lockKey);
  74.             }
  75.         }
  76.         return "end";
  77.     }
  78.     }
复制代码
代码重点解释
1.使用setnx
SETNX 是 Redis 数据库中的一个下令,用于将一个键值对(key-value pair)设置到 Redis 中,但只有在该键不存在的情况下才会设置乐成。假如该键已经存在,SETNX 下令不会对其举行任何操作,并返回 0,否则返回 1。
2.死锁问题
使用setnx举行加锁的时候,肯定要设置锁的逾期时间。业务完成之后,肯定要及时开释锁,克制产存亡锁问题。并且肯定要保证加锁和设置锁的逾期时间操作是原子的,克制只上锁,未设置逾期时间问题的存在
3.锁续命问题
上述代码作为一个简朴的分布式锁实现,在并发量不算很高的情况下,不会出现什么问题,但是它现实上照旧有瑕疵的。我们上述代码,锁失效有两种可能。一种是逾期,另一种是代码删除。代码删除没什么问题,我们选择将锁删除的时候,肯定是业务代码实行完毕。但是假如是逾期的话,有可能我们的业务代码还没有实行完,锁先逾期了,并发量大的情况下,外部不停有请求试图加锁,可能会造成锁失效的情况。
在没有添加分布式锁之前,两个服务器8021 和 8023会出现,超卖现象,具体超卖寄义是什么我就不解释了。
超卖现象:

我们发现,这两个服务固然单独的看,贩卖的商品都是正确的,但是放在一起看,就会发现有雷同的库存,这就说明,同一个库存被卖了两次,我们上文提到的超卖问题仍然存在!
下面对超卖现象举行办理,介绍一下办理过程,实现分布式锁之后的库存增减效果:
起首设置上100的库存数据(在redis缓存中)

发送并发压测请求 http://localhost:80/test/lock
可看到我们访问的是80端口,并不是8021 和 8023,这里就乐成的应用到了nginx的动态代理转发。
我先用下平凡的测试:
访问两次的测试结果(两个服务器轮询处置惩罚
第一次访问:

第二次访问:

而且可以很明确的看到,每一次获取的所的值是不一样的。
测试结果:

8023模块处置惩罚日志:
  1. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  2. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  3. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  4. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  5. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  6. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  7. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  8. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  9. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  10. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  11. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  12. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  13. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  14. =====>8023  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值= a849bddd-6b78-4bd5-9fc1-dba39deaee2a时间:1721698799461 即将扣减库存
  15. ====>拿到商品库存,库存数量 = 89
  16. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  17. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  18. *****线程睡眠,模仿业务花费的时间1毫秒
  19. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  20. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  21. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  22. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  23. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  24. 扣减成功,剩余的库存为:88
  25. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  26. =====>8023释放锁成功
  27. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  28. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  29. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  30. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  31. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  32. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  33. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  34. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  35. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  36. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  37. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  38. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  39. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  40. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  41. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  42. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  43. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  44. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  45. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  46. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  47. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  48. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  49. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  50. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  51. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  52. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  53. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  54. =====>8023  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
复制代码
8021模块处置惩罚日志:
  1. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  2. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  3. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  4. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  5. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  6. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  7. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  8. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  9. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  10. =====>8021  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值=db91e932-9325-4521-8d19-0d6f4b1794fb时间:1721698799443  即将扣减库存
  11. =====>拿到商品库存,库存数量 = 90
  12. *****线程睡眠,模仿业务花费的时间1毫秒
  13. 扣减成功,剩余的库存为:89
  14. ======>8021释放锁成功
  15. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  16. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  17. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  18. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  19. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  20. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  21. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  22. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  23. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  24. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  25. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  26. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  27. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  28. =====>8021  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值=7dbab45b-bff9-485d-9a5a-1fed1f12ac06时间:1721698799480  即将扣减库存
  29. =====>拿到商品库存,库存数量 = 88
  30. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  31. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  32. *****线程睡眠,模仿业务花费的时间1毫秒
  33. 扣减成功,剩余的库存为:87
  34. ======>8021释放锁成功
  35. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  36. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  37. =====>8021  对 产品ID lock:product:101 分布式锁加锁成功 ,锁值=81441999-f474-4d73-8d45-9f57d1d52b27时间:1721698799493  即将扣减库存
  38. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  39. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  40. =====>拿到商品库存,库存数量 = 87
  41. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  42. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  43. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  44. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  45. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  46. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  47. *****线程睡眠,模仿业务花费的时间1毫秒
  48. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  49. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  50. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  51. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  52. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  53. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  54. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  55. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  56. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  57. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  58. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  59. 扣减成功,剩余的库存为:86
  60. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
  61. ======>8021释放锁成功
  62. =====>8021  对 产品ID lock:product:101 分布式锁加锁失败,请重新操作下单
复制代码
可以看到两个服务器,一共只有4次扣减商品乐成,其余的96次均获取锁失败,导致扣减库存失败,提示用户重新操作。
实现了分布式锁的效果,而且不会出现超卖的情况。

四、实现分布式锁中 几个需要注意的地方::

注意点1:防止死锁

问题:死锁

   设置分布式锁的时候,需要给锁设置上锁的逾期时间,假设线程1通过SETNX获取到锁并且正常实行然后开释锁那么统统ok,其它线程也能获取到锁。但是线程1现在"耍脾气"了,线程1抱怨说"工作太久有点累需要休息一下,你们想要获取锁等着吧,等我把活干完你们再来获取锁"。此时其它线程就无法向下继承实行,因为锁在线程1手中。这种长期不开释锁情况就有可能造成死锁。
  需要注意的是,千万不能把“获取锁”和“设置超时时间”在代码中分成两步实行

缘故原由在于这两个步调分开实行没有保证原子性,拿锁到设置逾期时间之间是存在时间差的,假如在这之间呆板宕机了照旧会存在上述问题,办理办法就是在占锁的同时设置逾期时间。
办理办法:

   为了防止像线程1这种"耍脾气"的现象发生,我们可以设置key的逾期时间来办理。设置逾期时间事后其它线程可不会惯着线程1,其它线程表示你要休息可以,休息了指定时间把锁让出来然后拍拍屁股走人,没人惯着你。
  但是这个逾期时间的设置也是需要根据现实的业务举行评估,不能任意设置,因为可能会出现这样的问题,在上述的代码中可以模拟出来,在扣减库存之前,8023模块就寝Thread.sleep(1000); 1秒,模拟现实业务耗费,8021模块就寝20s,模拟业务消耗。假如我只是设置逾期时间是10s的话,那假如请求轮训到8021模块了,比及锁逾期了,还没睡醒,此时当前实行的任务就没有锁了,其他的任务就可以重新持有锁了,此时比及8021中的锁逾期的线程实行完任务(睡醒了)之后,他会删除锁,假如不判断是谁的锁,是不是他自己的锁,就会产生误删的情况,所以这就引申出了两个需要考虑的点,第一个:逾期时间设置的考虑,第二个:删除锁之前需要判断是否是自己的锁!
这两个问题在这个内里都有提到办理办法,希望能帮到各人☺
注意点2: 误删情况

问题:误删情况情况一:

   设置逾期时间线程1被治得服服帖帖,此时线程1又开始不妥人了。线程1想既然你抢我得锁,等你获得锁后我就将锁删除毕竟我还要有备用钥匙,让你也锁不住,让其它线程也实行。 线程1休息的时间超过了逾期时间,此时锁会主动开释。线程2现在脱颖而出抢到了锁然后开心的继承实行。但是现在线程1醒了,发现线程2抢走了锁。线程1表示小子胆挺肥啊,敢抢我的锁,等我实行完了就将你锁删除,让其它"哥们"也进来。此时就会发生蝴蝶效应,线程1删除了线程2的锁,线程2删除了线程3的锁,直到最后一个"哥们:wc,我锁了?"。当然线程是无感知,实在线程1乃至其它线程都不知道删除的是别人的锁,全部线程都以为删除的是自己的锁。直到最后一个线程无锁可删。 这种误删锁的情况让锁的存在荡然无存,原来应该串行实行的线程,在肯定程度上都开始并发实行了。 那么误删情况该如何办理了?
  办理办法:

   我们可以给锁加上线程标识,只有锁是当前线程的才气删除,否则不能删除。在添加key的时候,key的value存储当前线程的标识,这个标识只要保证唯一即可。可以使用UUID或者一个自增数据。在删除锁的时候,将线程标识取出来举行判断,假如雷同就表示锁是自己的能够删除,否则不能删除。
  我的办理办法:
最后删除锁的时候,我这里使用了先判断是否是当前处置惩罚的服务的本次处置惩罚设置的分布式锁,假如是,才删除,否则不让删除其他线程的服务处置惩罚产生的锁,这里肯定需要注意,否则会产生锁误删的情况,会让分布式锁失效!
我最初的写法是 (这个是会导致误删):
  1. finally {
  2.             //删除掉锁,防止死锁
  3.                 System.out.println("=====>8023释放锁成功" );
  4.                 stringRedisTemplate.delete(lockKey);
  5.         }
复制代码
新的实现方式(这个可以克制误删):
  1. finally {
  2.             //如果是自己加的锁就自己删掉,防止死锁
  3.             if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
  4.                 System.out.println("=====>8023释放锁成功" );
  5.                 stringRedisTemplate.delete(lockKey);
  6.             }
  7.         }
复制代码
另有一种办理办法:

获取锁
  1. //获取线程前缀,同时也是线程表示。通过UUID唯一性
  2. private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
  3. //与线程id组合
  4. public boolean tryLock(long timeOut) {
  5.         //获取线程id
  6.         String id =ID_PREFIX+ Thread.currentThread().getId();
  7.         //获取锁
  8.         Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, id , timeOut, TimeUnit.SECONDS);
  9.         return Boolean.TRUE.equals(absent);
  10.     }
复制代码
开释锁
  1. public void unLock() {
  2.                 //获取存储的线程标识
  3.         String value = stringRedisTemplate.opsForValue().get(key);
  4.         //当前线程的线程标识
  5.         String id =ID_PREFIX+ Thread.currentThread().getId();
  6.         //线程标识相同则删除否,则不删除
  7.         if (id.equals(value)){
  8.             redisTemplate.delete(key);
  9.         }
  10.     }
复制代码
问题:误删情况情况二

问题

加入线程标识后,线程一不能任意删除其它线程的锁,但是线程1又开始不妥人了。线程1表示判断线程标识和开释锁的操作我可以分开实行,这又不是一个原子性的操作,线程1干完活以后就预备去开释锁,当线程1判断锁是自己的后表示开锁太累了,休息一会在开。此时其它线程就想无所谓,反正逾期时间一到锁就会主动开释。但是线程1已经判断了锁是自己的以后就不会实行判断锁的操作(线程1已经实行了if判断,只是没有实行方法体),当线程2获得锁后,线程1仍然能删除线程2的锁。
   解锁时,查 - 删 操作是 2 个操作,由两个下令完成,非原子性。
redis底层实行这个setnx不是一个原子操作,而是有两步操作完成的,起首set hello world,然后第二步设置key的逾期时间:
expire hello 3,那么假如实行完第一步刚好redis宕机了,此时key一直保存到redis。永远也无法删除了。
  办理办法:待定

在redis实现的分布式锁中,这种因为服务器可能存在的宕机导致的误删情况是无法预推测的。
下一篇我们将通过Redisson实现分布式锁。这个在一样平常中较最为常用。
路漫漫其修远兮,吾必将上下求索~
到此关于Redis实现分布式锁就算告一段落了,假如你以为博主写的不错!写作不易,请点赞、关注、批评给博主一个鼓励吧转~
参考链接:https://blog.csdn.net/zhuge_long/article/details/137347203
参考链接:https://blog.csdn.net/hlzdbk/article/details/129940116 、
参考链接:https://blog.csdn.net/h2503652646/article/details/118977164

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

飞不高

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表