从0~1实现分布式锁演变过程

从0~1实现分布式锁演变过程

工作中用到分布式锁的场景并不多(行业性质决定),后来在一幅漫画中了解到分布式锁的实现思路对我影响挺深的,在经过一系列的查阅资料后终于决定从0到1将分布式锁的过程演变一次。

点这儿获取完整代码

首先看一段模仿减库存代码:

    @GetMapping("/getStock")
    public String getStock() {
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
        return "操作成功!";
    }

以上代码很简单,就是从redis中取库存并判断是否大于0(有库存),有库存则减少1,最后更新redis中的库存。
以上代码在单线程中是没有问题,但如果在多线程的情况下就会出现重复减库存现象,这里我用的是JMeter模仿高并发场景,参数配置及压测结果如下:

线程组配置,参数如图所示
在这里插入图片描述
请求参数配置,http://www.com.zxf/getStock 是我本地自己用Nginx 配置的反向代理和负载均衡
在这里插入图片描述
Nginx主要配置如下

#配置上游服务器网关端口集群
	upstream	backServer{
		server	127.0.0.1:8081 weight=1;
		server	127.0.0.1:8082 weight=1;
	}

    server {
        listen       80;
        server_name  www.com.zxf;
        #charset koi8-r;
        #access_log  logs/host.access.log  main;
        location / {
            #root   html;
			proxy_pass http://backServer/;
            index  index.html index.htm;
        }
       
    }

压测结果
在这里插入图片描述
如上图所示,在多线程情况下会出现重复减库存现象,出现这种情况通常做法是加锁,加锁的目的是为了在多线程情况下每次只能有一个线程去减库存,等到该线程处理完释放锁后其他线程再继续执行,修改后的代码如下:

    @GetMapping("/getStock")
    public String getStock() {
        synchronized (this){
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        }
        return "操作成功!";
    }

压测结果如下:

扣减成功,剩余库存:49
扣减成功,剩余库存:48
扣减成功,剩余库存:47
扣减成功,剩余库存:46
扣减成功,剩余库存:45
扣减成功,剩余库存:44
扣减成功,剩余库存:43
扣减成功,剩余库存:42
扣减成功,剩余库存:41
扣减成功,剩余库存:40
扣减成功,剩余库存:39
扣减成功,剩余库存:38
扣减成功,剩余库存:37
扣减成功,剩余库存:36
扣减成功,剩余库存:35
扣减成功,剩余库存:34
扣减成功,剩余库存:33
扣减成功,剩余库存:32
扣减成功,剩余库存:31
扣减成功,剩余库存:30
扣减成功,剩余库存:29
扣减成功,剩余库存:28
扣减成功,剩余库存:27
扣减成功,剩余库存:26
扣减成功,剩余库存:25
扣减成功,剩余库存:24
扣减成功,剩余库存:23
扣减成功,剩余库存:22
扣减成功,剩余库存:21
扣减成功,剩余库存:20
扣减成功,剩余库存:19
扣减成功,剩余库存:18
扣减成功,剩余库存:17
扣减成功,剩余库存:16
扣减成功,剩余库存:15
扣减成功,剩余库存:14
扣减成功,剩余库存:13
扣减成功,剩余库存:12
扣减成功,剩余库存:11
扣减成功,剩余库存:10
扣减成功,剩余库存:9
扣减成功,剩余库存:8
扣减成功,剩余库存:7
扣减成功,剩余库存:6
扣减成功,剩余库存:5
扣减成功,剩余库存:4
扣减成功,剩余库存:3
扣减成功,剩余库存:2
扣减成功,剩余库存:1
扣减成功,剩余库存:0
扣减失败,库存不足
扣减失败,库存不足

如上,在使用synchronized后解决了重复减库存现象,但是又引来了新的问题:synchronized作用是在JVM层,也就是在单体中生效,而我们的服务布署一般都是多节点分布式布署,举个例子,假如布署两个结点,每个结点中都有各自的JVM,而synchronized只是在自己结点中的JVM生效,同样也会出现问题,为了验证上面的问题,我启动两个服务,端口分别是8081和8082,验证如下,

8081端口

  
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.0)

2021-08-13 12:26:35.416  INFO 7540 --- [           main] com.zxf.DistributeApplication            : Starting DistributeApplication using Java 1.8.0_45 on DESKTOP-KB6ANRO with PID 7540 (H:\IDEA\workspace\distribute\target\classes started by Administrator in H:\IDEA\workspace\distribute)
2021-08-13 12:26:35.420  INFO 7540 --- [           main] com.zxf.DistributeApplication            : No active profile set, falling back to default profiles: default
2021-08-13 12:26:36.596  INFO 7540 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
2021-08-13 12:26:36.600  INFO 7540 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2021-08-13 12:26:36.628  INFO 7540 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 7 ms. Found 0 Redis repository interfaces.
2021-08-13 12:26:37.583  INFO 7540 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8081 (http)
2021-08-13 12:26:37.604  INFO 7540 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-08-13 12:26:37.604  INFO 7540 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.39]
2021-08-13 12:26:37.745  INFO 7540 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-08-13 12:26:37.745  INFO 7540 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2148 ms
2021-08-13 12:26:38.908  INFO 7540 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-08-13 12:26:39.415  INFO 7540 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8081 (http) with context path ''
2021-08-13 12:26:39.431  INFO 7540 --- [           main] com.zxf.DistributeApplication            : Started DistributeApplication in 4.953 seconds (JVM running for 5.868)
2021-08-13 12:30:45.410  INFO 7540 --- [io-8081-exec-59] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-08-13 12:30:45.426  INFO 7540 --- [io-8081-exec-59] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2021-08-13 12:30:45.426  INFO 7540 --- [io-8081-exec-59] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
扣减成功,剩余库存:49
扣减成功,剩余库存:48
扣减成功,剩余库存:47
扣减成功,剩余库存:46
扣减成功,剩余库存:45
扣减成功,剩余库存:44
扣减成功,剩余库存:43
扣减成功,剩余库存:42
扣减成功,剩余库存:41
扣减成功,剩余库存:40
扣减成功,剩余库存:39
扣减成功,剩余库存:38
扣减成功,剩余库存:37
扣减成功,剩余库存:36
扣减成功,剩余库存:35
扣减成功,剩余库存:34
扣减成功,剩余库存:33
扣减成功,剩余库存:32
扣减成功,剩余库存:31
扣减成功,剩余库存:30
扣减成功,剩余库存:29
扣减成功,剩余库存:28
扣减成功,剩余库存:26
扣减成功,剩余库存:25
扣减成功,剩余库存:24
扣减成功,剩余库存:23
扣减成功,剩余库存:22
扣减成功,剩余库存:21
扣减成功,剩余库存:20
扣减成功,剩余库存:21
扣减成功,剩余库存:20
扣减成功,剩余库存:19
扣减成功,剩余库存:18
扣减成功,剩余库存:16
扣减成功,剩余库存:15
扣减成功,剩余库存:14
扣减成功,剩余库存:14
扣减成功,剩余库存:13
扣减成功,剩余库存:12
扣减成功,剩余库存:11
扣减成功,剩余库存:10
扣减成功,剩余库存:9
扣减成功,剩余库存:7
扣减成功,剩余库存:6
扣减成功,剩余库存:5
扣减成功,剩余库存:4
扣减成功,剩余库存:3
扣减成功,剩余库存:2
扣减成功,剩余库存:1
扣减成功,剩余库存:0
扣减失败,库存不足

8082端口

   .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.0)

2021-08-13 12:26:52.642  INFO 5456 --- [           main] com.zxf.DistributeApplication            : Starting DistributeApplication using Java 1.8.0_45 on DESKTOP-KB6ANRO with PID 5456 (H:\IDEA\workspace\distribute\target\classes started by Administrator in H:\IDEA\workspace\distribute)
2021-08-13 12:26:52.648  INFO 5456 --- [           main] com.zxf.DistributeApplication            : No active profile set, falling back to default profiles: default
2021-08-13 12:26:53.802  INFO 5456 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
2021-08-13 12:26:53.806  INFO 5456 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2021-08-13 12:26:53.834  INFO 5456 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 7 ms. Found 0 Redis repository interfaces.
2021-08-13 12:26:54.907  INFO 5456 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8082 (http)
2021-08-13 12:26:54.926  INFO 5456 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-08-13 12:26:54.926  INFO 5456 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.39]
2021-08-13 12:26:55.091  INFO 5456 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-08-13 12:26:55.092  INFO 5456 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2254 ms
2021-08-13 12:26:56.296  INFO 5456 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-08-13 12:26:56.709  INFO 5456 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8082 (http) with context path ''
2021-08-13 12:26:56.725  INFO 5456 --- [           main] com.zxf.DistributeApplication            : Started DistributeApplication in 5.081 seconds (JVM running for 6.427)
2021-08-13 12:30:45.894  INFO 5456 --- [nio-8082-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-08-13 12:30:45.894  INFO 5456 --- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2021-08-13 12:30:45.910  INFO 5456 --- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 16 ms
扣减成功,剩余库存:37
扣减成功,剩余库存:32
扣减成功,剩余库存:30
扣减成功,剩余库存:27
扣减成功,剩余库存:26
扣减成功,剩余库存:24
扣减成功,剩余库存:22
扣减成功,剩余库存:19
扣减成功,剩余库存:17
扣减成功,剩余库存:15
扣减成功,剩余库存:13
扣减成功,剩余库存:11
扣减成功,剩余库存:8
扣减成功,剩余库存:7
扣减成功,剩余库存:6
扣减成功,剩余库存:5
扣减成功,剩余库存:3
扣减成功,剩余库存:1
扣减成功,剩余库存:0
扣减失败,库存不足

如上结果显示,在两结点中公有37、32、30等库存字样,从而说明上面的问题是存在的。

那么怎么解决这个问题呢,首先,还是加锁的概念,加锁的目的是为了在多线程情况下每次只能有一个线程去减库存,只不过这儿的锁换成了redis锁而非JVM中的同步锁。

修改后的代码如下:

    @GetMapping("/getStock")
    public String getStock() {
        String lockKey = "lockKey_stock";
        try {
            //setIfAbsent 相当于setnx(key, vlue),只有在key不存在时设置key的值,key存在时返回false
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lockKey_stock");
            stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
            if (!result) {
                return "error";
            }
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            // 释放锁
            stringRedisTemplate.delete(lockKey);
        }
        return "操作成功!";
    }

如上所示代码,有没有什么问题呢,或者有什么可以改进的地方,答案是肯定的,首先,
问题一:先加锁,再设置过期时间就有问题,如果加锁成功后由于不可抗力因素(redis宕机)没有成功设置过期时间,那么就会一致处于被锁状态;
问题二:默认过期时间为10秒,假设线程一进来处理业务需要15秒,10秒后redis自动释放锁,这时线程二进来一看没有锁,于是线程二加锁,这时恰好第一个线程业务处理完释放锁,注意:此时线程一释放的是线程二的锁,以此类推会导致锁失效问题。

针对以上两点问题代码优化如下:

    @GetMapping("/getStock")
    public String getStock() {
        String lockKey = "lockKey_stock";
        String threadId = String.valueOf(Thread.currentThread().getId());
        try {
            //setIfAbsent 相当于setnx(key, vlue),只有在key不存在时设置key的值,key存在时返回false
            /*Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lockKey_stock");
            stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);*/
            // 在添加锁的同时设置过期时间
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, threadId, 10, TimeUnit.SECONDS);
            if (!result) {
                return "error";
            }
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            // 释放锁,解决锁失效问题
            if (threadId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "操作成功!";
    }

针对问题1解决方案:在添加锁的同时设置过期时间,这时一个原子性操作,要么同时成功,要么同时失败;
针对问题2解决方案:在加锁时获取当前线程ID,在释放时判断当前线程跟加锁的线程是否一致。

再仔细考虑一下,设置的锁的默认过期时间是10秒,但是如果处理业务代码超过10秒依然会释放,能不能做到让程序自动去判断是否要给当前锁延续寿命,
思路:后台写一个定时器定时监控当前线程是否还持有锁,如果持有说明业务逻辑还没有处理完,自动更新过期时间!

其实对于以上的一切Redisson框架已经帮我们全部实现了!其流程如图所示
在这里插入图片描述

引入依赖

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.6.5</version>
</dependency>

实现代码

@GetMapping("/getStock")
public String getStock() {
  String lockKey = "lockKey_stock";
  RLock redissonLock = redisson.getLock(lockKey);
  try {
      redissonLock.lock(10, TimeUnit.SECONDS);
      int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
      if (stock > 0) {
          int realStock = stock - 1;
          stringRedisTemplate.opsForValue().set("stock", realStock + "");
          System.out.println("扣减成功,剩余库存:" + realStock);
      } else {
          System.out.println("扣减失败,库存不足");
      }
  } finally {
      // 释放锁,解决锁失效问题
      redissonLock.unlock();
  }
  return "操作成功!";
}

如上,使用Redisson实现分布式锁只需要简单的三步就可以做好上面写的一切,而且还更加完美。

获取锁:RLock redissonLock = redisson.getLock(lockKey);
设置初始过期时间:redissonLock.lock(10, TimeUnit.SECONDS);
释放锁:redissonLock.unlock();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值