前言
本文力争以最简单的语言, 以博主自己对分布式锁的理解, 按照自己的语言来描述分布式锁的概念, 作用, 原理, 实现. 如有错误, 还请各位大佬海涵, 恳请指正. 分布式锁分两篇来讲解, 本篇讲解客户端, 下一篇讲解 Redis 服务端.
概念
如果把分布式锁的概念搬到这里, 博主也会觉得枯燥. 博主这里以举例的形式来描绘它.
试想一种场景, 在一个偏远小镇上的火车站, 只有一个售票窗口.
火车站来了 10 名旅客, 前往售票窗口购买火车票, 旅客只能排队购票, 排到第一的旅客, 可以与售票员沟通, 买票.
好啦, 以上就是一个分布式锁的场景, 我们来分析一下每一个细节.
每位旅客可以理解为一个系统或者线程. 他们在竞争售票员的工作时间.
是不是觉得分布式锁也不是什么高大上的概念. 有同学会问, 锁到底在哪里呢? 还是买票场景, 我们看看锁长什么样子.
我们深入想一下, 这 10 位旅客本来是并行的(没有买票前, 他们有的在吃饭, 有的在玩手机, 等等等), 而到了买票的时候, 就必须排队(串行), 而不是一起买票.
没错, 就是在特定的场景下, 将并行的场景, 变成串行, 就是分布式锁的奥义所在.
作用
分布式锁的作用不但非常大, 而且非常多.
在软件设计中, 比如电商秒杀活动. 商家预备了 1000 件货物, 也就只有这 1000 件货, 有 1500 人参与秒杀, 可以理解为 1500 个线程来排队购买商品. 那就必须将这 1500 个线程排个队(比如按照时间), 设置一把锁, 一个购买过程结束, 再开始下一个.
为什么 Redis 可以实现分布式锁呢?
我们以购票举例, 购票窗口前的这个锁, 是每位旅客都可以看到的.
这里我们可以得出一个结论, 一把锁首先要具有的属性是: 想要获得锁的人都可以看到.
这把锁既不能属于服务器 A, 也不能属于服务器 B, 因为他们都不知道另一方的存在, 那就必须选择一个公信的第三方来作为锁. 当当当~ Redis 闪亮登场. 当然 zookeeper 也可以实现, 这里先挖一个坑, 以后再填 zookeeper 吧.
原理
加锁的基本思路
Redis 中有一条指令非常有意思, 它叫做 setnx
当 Redis 中不存在 key 值为 "lock" 的时候, 可以设置成功; 当存在 key 值时, 设置失败.
这句指令, 好比是, 询问一下, 到我买票了吗? 返回结果是 1 的时候, 到您买票了; 返回结果是 0 的时候, 还没到您, 稍后再询问.
我们的锁过程可以这样来操作:
setnx lock 锁值
处理业务逻辑
释放锁 del lock
优化一
为什么要优化?
试想, 如果 setnx lock 1 加锁成功, 这个时候系统因为其他原因, 挂掉了, 就永远无法执行 del lock 了.
要避免这种情况, 怎么办呢? 给锁一个过期时间.
这样无论系统是否宕机, 都会在 10 秒后释放锁. 看似很美好, 虽然 setnx lock 1 与 expire lock 10 之间的时间间隙非常小, 但仍然有风险, 加入系统执行完 setnx lock 1 后, 宕机了, 并没有执行 过期指令 expire lock 10, 再次产生了一把无法解开的锁,"死锁".
这时候引入了一个概念, 叫做原子操作. 即这两条指令需要在一个原子操作内执行完成.
set key value [expiration EX seconds|PX milliseconds] [NX|XX]
优化二
why? 上一个优化已经把上锁过程做成了原子操作, 还需要什么优化呢?
当然有, 试想一下, 之前代码 set lock 1 ex 10 nx, 设置过期时间是 10 秒, 那么这个 10 秒是否可靠呢? 显然不可靠.
我们加锁的过程是 加锁 --- 执行业务代码 --- 释放锁
加入业务代码的执行时间超过 10 秒呢? 是不是业务代码还没有执行完, 锁就已经释放了. 放在购票场景中, 第一位旅客还没有完成购票, 第二位旅客就开始购票. 显然不合理. 怎么办呢?
这里我们需要估计业务代码的执行时间, 加入预估出来的时间是 10 秒, 可以在业务代码中开辟一个 "续命" 的操作.
加锁 set lock 1 ex 10 nx
每过 3 秒, 把该锁的时间重新设置为 10 秒
执行业务代码
释放锁 del lock
这里的续命时间间隔 = 过期时间 10S / 3
这样设置比较合理, 可以防止一次续命失败.
优化三
纳尼? 还有问题吗?
有, 而且可以算是一个 bug, 我们一直在用 set lock 1 ex 10 nx 来加锁, 用 del lock 来释放锁.
我们需要明确知道, 释放的锁, 是自己加上的.
可以 set lock uuid ex 10 nx 来解决该问题.
拓展 - 可重入锁
一个线程获取到锁以后, 再次获取锁, 就是可重入锁.
但博主现在遇到的问题, 一般不需要可重入锁即可解决. java 中 ReentrantLock 就是可重入锁.
可重入锁, 对代码的复杂度增加了很多, 玩不好, 容易扯裆. 谨慎使用.
实现
已经讲了很多优化相关的内容, 这里博主就直接写优化后的代码了.
博主使用 java 来实现. 而 Redis 官方 (https://redis.io/clients#java) 推荐的有三个框架. 分别是 Jedis,lettuce,Redisson.
由于博主在本篇中主要讨论单个 Redis 的情况, 而 redisson 主要用来处理分布式 Redis, 下一篇博文使用 redisson, 敬请期待.
springboot2.x 默认采用了 lettuce, 所以博主就使用 lettuce 来实现分布式锁.
引入依赖
- <!-- data-redis 中集成了 lettuce -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-Redis</artifactId>
- </dependency>
- <!-- redis 链接池 -->
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-pool2</artifactId>
- </dependency>
- <!-- alibaba json -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.72</version>
- </dependency>
配置文件
既然要测试分布式锁, 那么就至少应该跑两份代码, 所以配置文件也应该是两份, 这里博主偷个懒, 提供一份配置文件, 另一份配置文件修改下 server 的端口即可.
- server:
- port: 80
- spring:
- Redis:
- # Redis 的 ip 地址
host: Redis 的 ip 地址
- # Redis 的端口号
- port: 6379
- # Redis 的密码
password: 你的密码
- lettuce:
- pool:
- # 最大链接数
- max-active: 30
- # 链接池中最大空闲链接数
- max-idle: 15
- # 最大阻塞等待链接时长 默认不限制 -1
- max-wait: 2000
- # 最小空闲链接数
- min-idle: 10
- # 链接超时时长
- shutdown-timeout: 10000
lettuce 配置类
这个类博主就不细讲了, springboot 整合 lettuce, 序列化博主更偏爱 FastJson
- import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.Redis.connection.RedisConnectionFactory;
- import org.springframework.data.Redis.core.RedisTemplate;
- import org.springframework.data.Redis.serializer.StringRedisSerializer;
- /**
- * @author xujp
- * Redis 配置类 将 RedisTemplate 交给 spring 托管
- */
- @Configuration
- public class RedisConfig {
- @Bean
- public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
- RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
- redisTemplate.setConnectionFactory(redisConnectionFactory);
- StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
- GenericFastJsonRedisSerializer genericFastJsonRedisSerializer = new GenericFastJsonRedisSerializer();
- redisTemplate.setKeySerializer(stringRedisSerializer);
- redisTemplate.setValueSerializer(genericFastJsonRedisSerializer);
- redisTemplate.setHashKeySerializer(stringRedisSerializer);
- redisTemplate.setHashValueSerializer(genericFastJsonRedisSerializer);
- redisTemplate.afterPropertiesSet();
- return redisTemplate;
- }
- }
分布式锁
重头戏来了, 手写分布式锁的核心代码示例.
- import com.Redis.demo1.thread.WatchDog;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.Redis.core.RedisTemplate;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.Web.bind.annotation.RequestMapping;
- import org.springframework.Web.bind.annotation.RestController;
- import java.util.UUID;
- import java.util.concurrent.TimeUnit;
- /**
- * @author xujp
- */
- @RestController
- @RequestMapping("/test")
- public class TestController {
- @Autowired
- private RedisTemplate redisTemplate;
- @GetMapping
- public void lock(){
- String uuid = UUID.randomUUID().toString();
- //System.out.println(uuid);
- WatchDog watchDog;
- try {
- // 自旋
- while (true) {
- // 尝试获取锁
- Boolean hasLock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3l, TimeUnit.SECONDS);
- if(hasLock) {
- // 看门狗 "续命"
- watchDog = new WatchDog(redisTemplate, uuid);
- watchDog.start();
- // 业务逻辑 start
- int num = (int) redisTemplate.opsForValue().get("num");
- //Thread.sleep(4000); // 假设业务需要 4s 处理时间
- redisTemplate.opsForValue().set("num", num - 1);
- System.out.println(num);
- // 业务逻辑处理 end
- break;
- }else{
- // 睡眠 100ms 再自旋
- Thread.sleep(100);
- }
- }
- }catch (Exception e){
- System.out.println(e);
- }finally {
- // 关闭锁
- String l = (String) redisTemplate.opsForValue().get("lock");
- if (l.equalsIgnoreCase(uuid)) {
- redisTemplate.delete("lock");
- }
- }
- }
- }
分布式锁 "续命" 代码示例
- import org.springframework.data.Redis.core.RedisTemplate;
- import java.util.concurrent.TimeUnit;
- /**
- * @author xujp
- */
- public class WatchDog extends Thread {
- private RedisTemplate redisTemplate;
- private String uuid;
- public WatchDog(RedisTemplate redisTemplate, String uuid){
- this.redisTemplate = redisTemplate;
- this.uuid = uuid;
- }
- public void run(){
- // 续命逻辑
- while (true){
- try {
- // 获取锁的 value
- Object redisUUID = redisTemplate.opsForValue().get("lock");
- // 判断当前父线程是否已经释放锁, 如果父线程已释放, 则跳出线程
- if(redisUUID==null || !redisUUID.toString().equals(uuid)){
- break;
- }
- // 续命
- redisTemplate.expire("lock", 3l, TimeUnit.SECONDS);
- // 没隔 1s 续命一次
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- break;
- }
- }
- }
- }
测试
首先我们将代码分别以 80 和 81 端口 run 起来.
有精力的同学, 还可以再搭建一个 nginx 将请求分流到 80 和 81. 这里博主简单粗暴地使用 jmeter 请求.
博主使用 jmeter 来测试, 博主默认大家都会使用(不会使用的童鞋需要学习喽).
jmeter 准备工作
在 jmeter 中设置 50 个线程
在该线程下设置两个接口, 分别请求 80 和 81
Redis 准备工作
在 Redis 中设置一对键值 num
至此, 就可以在 jmeter 中开启请求了
测试结果
我们先来看 Redis 中 num 的值
我们再分别查看 80 和 81 的日志
总结
本文讲述了利用 Redis 实现分布式锁的原理, 分布式锁本质上是将并发请求按顺序处理, 那么这把锁就成为了所有请求的瓶颈, 如何打破锁的瓶颈呢? 敬请关注博主, 后续填坑(博主挖坑必填).
本文留下的两个坑:
1, 为了使 Redis 高可用, Redis 集群后, 如何解决 Redis 端因为网络问题导致锁不同步问题?
2, 分布式锁实现了并发排队, 锁成为了性能瓶颈, 如何提高性能?
来源: https://www.cnblogs.com/x-j-p/p/13322022.html