本文已收录于专栏
🍅《Redis从入门到进阶》🍅
专栏前言
本专栏开启,目的在于帮助大家更好的掌握学习
Redis
,同时也是为了记录我自己学习
Redis
的过程,将会从基础的数据类型开始记录,直到一些更多的应用,如缓存击穿还有分布式锁等。希望大家有问题也可以一起沟通,欢迎一起学习,对于专栏内容有错还望您可以及时指点,非常感谢大家 🌹。
目录
1.什么是分布式锁?
锁这个东西,大家都知道,在我们
jvm
内部多个线程竞争同一个资源时,我们利用
jvm
提供的
synchronized
或者一些其他的锁可以帮助我们让线程对资源的串行使用。但这种方法并不适合现在企业广泛使用的分布式架构。因为在这种集群模式下,
jvm
内部的锁无法被其他
jvm
内部感知到,那这样肯定无法满足我们的要求,因为锁肯定是要被大家都能感知到的,所以分布式锁应用而生。以前的锁竞争对象是线程之间,而分布式系统中竞争共享资源的单位从线程升级为了进程。
2. 分布式锁的条件
那么作为一个分布式锁,它应该具备哪些条件呢?
- 可见性:多个进程之间均可以看见该锁,且可以尝试获取该锁
- 互斥性:锁最基本的特性,同一时间只能保证锁被一个进程持有
- 高可用性:也可以理解为容错性,当提供锁的服务结点产生故障时,程序不会因为守到强烈影响
- 高性能:锁的释放和添加本身十分消耗性能,我们应选择性能较好的锁
3.常见的分布式锁
我们一般常见的分布式锁,有以下三种:
- MySQL:
MySQL
自带锁机制,但由于其性能一般,所以作为分布式锁比较少见 - Redis:
Redis
是分布式锁一种非常常见的实现方式,我们可以利用setnx
这个方法,如果插入成功表示锁获取成功,否则获取失败。利用这个机制完成互斥,从而实现分布式锁,而且Redis
存储在内存中本身就符合高性能特点。 - Zookeeper:
Zookeeper
也是企业开发中较好的一种实现分布式锁的方案,以后有机会讲解。
4.Redis 实现分布式锁
基于
Redis
实现分布式锁,我们使用两个方法:
- 1.获取锁 该指令会插入一个结构为
lock:thread01
的锁,且超时时间为100
秒,返回值为OK
说明获取锁成功,失败则返回false
,该方法不会进行阻塞。 - 2.释放锁 通过手动删除该锁来进行释放,或者可以等待
TTL
让该锁自动过期
核心思路:
利用
Redis
的
SETNX
方法,当多个进程同时竞争该锁时,都会调用该方法获取锁,只有一个进程成功能成功获取成功,此时
Redis
将会生成该锁,其他进程获取失败。那么获取锁成功的进程将会去执行业务,最后删除该锁,这样就将锁释放了。如果想让获取锁失败的进程重新获取,可以手动休眠一段时间后重新获取。
下面是一个简单的使用
StringRedisTemplate
实现分布式锁的代码:
publicclassDistributedLock{privateStringRedisTemplate redisTemplate;privateString lockKey;privateString lockValue;privatelong lockTimeout;//构造方法publicDistributedLock(StringRedisTemplate redisTemplate,String lockKey,String lockValue,long lockTimeout){this.redisTemplate = redisTemplate;//keythis.lockKey = lockKey;//valuethis.lockValue = lockValue;//过期时间this.lockTimeout = lockTimeout;}publicbooleantryLock(){// 尝试获取锁Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, lockTimeout,TimeUnit.MILLISECONDS);return result !=null&& result;}publicvoidunlock(){// 释放锁
redisTemplate.delete(lockKey);}}
5. 分布式锁误删问题
上面的逻辑看上去完美无缺,但还是存在很严重的问题,考虑下面一个场景:
逻辑说明:
线程
A
持有锁执行业务的时候发生了堵塞,导致他的锁
TTL
到期自动释放了,此时线程
B
成功获取到锁了,因为线程
A
已经释放该锁了。这时候线程
A
阻塞完毕后继续执行完业务,然后删除该锁,线程
B
执行完业务时突然发现——woc 我锁呢? 它的锁被线程
A
误删了,这就是分布式锁误删问题。
解决方案:
上诉问题的产生主要原因,还是因为每个线程并不知道该锁是不是自己的,那我们可以在删除锁的时候去加以判断,如果该锁不属于自己,则不删除该锁。如果该锁是自己的且还未到期,再进行删除锁。我们这里标识一把锁的时候同时存入线程的
ID
,一般在同一个
jvm
中线程的标识一般不相同,但我们这是在集群模式下,所以也有可能出现
ThreadID
重复的情况,所以我们可以考虑在前面拼接上一个
UUID
。
privatestaticfinalString ID_PREFIX = UUID.randomUUID().toString(true)+"-";@OverridepublicbooleantryLock(long timeoutSec){// 获取线程标识String threadId = ID_PREFIX +Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec,TimeUnit.SECONDS);returnBoolean.TRUE.equals(success);}@Overridepublicvoidunlock(){// 获取当前线程的标识String threadId = ID_PREFIX +Thread.currentThread().getId();// 获取锁中的标识String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断标识是否一致if(threadId.equals(id)){// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);}}
6. 分布式锁原子性问题
在进行区分锁的处理后,那么是不是一定不会产生问题呢?
考虑一种更加极端的情况,当线程
A
判断完标识发现一致后,准备释放锁的时候又突然出现了阻塞情况(比如
JVM
垃圾挥手),锁又到期了,线程
B
进来拿了一把锁,因为线程
A
已经判断完标识,所以它一删锁又把
B
的锁给删掉了,这就又产生了误删的问题。
解决的方案需要我们使用
Lua
脚本,来保证拿锁、判断标识、删锁三个操作是一个原子性操作,而
Lua
脚本可以同时执行多条
Redis
指令并且保证原子性,
Lua
脚本是一门脚本语言,有兴趣可以自行了解一下。
版权归原作者 执 梗 所有, 如有侵权,请联系我们删除。