当前位置:网站首页>基于Redis的分布式锁 以及 超详细的改进思路
基于Redis的分布式锁 以及 超详细的改进思路
2022-07-06 09:17:00 【阿杆.】
文章目录
基于Redis的分布式锁
什么是分布式锁
满足分布式系统或集群模式下多进程可见并且互斥的锁
需要实现的两个基本方法
获取锁
互斥:确保只能有一个线程获取锁,可以利用
setnx
的互斥特性非阻塞:尝试一次,成功返回true,失败返回false
释放锁
手动释放,
DEL key
超时释放:获取锁时添加一个超时时间,避免服务宕机引起的死锁,
EXPIRE lock 10
组合
SET lock thread1 NX EX 10 # NX是互斥、EX是设置超时时间
实现思路
第一版代码
package cn.sticki.common.redis.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/** * @author 阿杆 * @version 1.0 * @date 2022/6/21 21:57 */
public class RedisSimpleLock implements ILock {
private final StringRedisTemplate stringRedisTemplate;
private final String name;
private final static String KEY_PREFIX = "lock:";
public RedisSimpleLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeout) {
// 1. 获取线程标识
long threadId = Thread.currentThread().getId();
// 2. 尝试写入redis
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeout, TimeUnit.SECONDS);
// 3. 返回写入情况,成功即获取到锁
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 1. 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
改进思路
存在的问题
存在一种情况,按照以下顺序执行:
- 线程1获取到锁,开始执行业务,执行过程中发生了阻塞,在阻塞的过程中,锁超时释放了
- 线程2获取锁,由于线程1的锁已经超时释放,所以线程2可以成功获取到锁
- 线程1业务执行完毕,释放锁,但线程1的锁早就超时释放了,所以释放的是线程2的锁
- 线程3获取锁,线程3和线程2并发执行,此时锁并未起到应起的作用。。。
示意图:
解决方案
这个问题,实际上就是释放了不是自己产生的锁,故我们可以通过特定的标识,在释放锁之前判断锁是否是由自己产生的,且只释放自己产生的锁。
可以将线程id存入value,在释放之前判断锁的value是否等于自己的线程id,若等于则说明该锁是当前线程产生的,可以释放。
新的问题
如果我有多个服务器,组成了一个集群,那么不同的服务器有可能出现线程id相同的情况,就会导致value相同,从而错误的释放了别人的锁。
解决方案
让每个启动的服务都有一个不同的标识,再拼接线程id,就可以解决这个问题。
最终方案
在获取锁时存入线程标识(可以用UUID + 线程id 表示)
在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致
如果一致则释放锁
如果不一致则不释放锁
第二版代码
package cn.sticki.common.redis.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/** * @author 阿杆 * @version 1.0 * @date 2022/6/21 21:57 */
public class RedisSimpleLock implements ILock {
private final StringRedisTemplate stringRedisTemplate;
private final String name;
private final static String KEY_PREFIX = "lock:";
private final static String KEY_UUID = UUID.randomUUID() + ":";
public RedisSimpleLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeout) {
// 1. 生成key,通过拼接前缀和业务名
String key = KEY_PREFIX + name;
// 2. 生成value,用于判断该锁是不是当前线程生成的。使用随机的UUID+当前线程id,防止集群时value碰撞。
String value = KEY_UUID + Thread.currentThread().getId();
// 3. 尝试写入redis
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
// 4. 返回写入情况,成功即获取到锁
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
String key = KEY_PREFIX + name;
String value = KEY_UUID + Thread.currentThread().getId();
// 1. 获取锁的值
String lockValue = stringRedisTemplate.opsForValue().get(key);
if (value.equals(lockValue)) {
// 2. 若值相同,则当前锁是由当前线程创建的,可以删除
stringRedisTemplate.delete(key);
}
}
}
新的问题
如果,我是说如果,在上面的unlock()代码中,获取到锁的值之后,删除key之前,发生了阻塞(GC阻塞),等阻塞完成后,当前线程创建的锁已经被释放了,然后发生了和上面类似的问题,也一样会导致锁的失效。
可能描述的不太清楚,看看示意图吧:
解决思路
产生这个问题的原因主要在于判断锁标识和释放锁是分别执行的两个操作,解决这个问题,可以通过Lua脚本将两个操作绑定在一起。
Redis的Lua脚本
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法可以参考网站:https://www.runoob.com/lua/lua-tutorial.html
这里重点介绍Redis提供的调用函数,语法如下:
# 执行redis命令
redis.call('命令名称', 'key', '其它参数', ...)
例如,我们要执行set name jack,则脚本是这样:
# 执行 set name jack
redis.call('set', 'name', 'jack')
例如,我们要先执行set name Rose,再执行get name,则脚本如下:
# 先执行 set name jack
redis.call('set', 'name', 'jack')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name
第三版代码
用Lua写释放锁
释放锁的业务流程是这样的:
- 获取锁中的线程标示
- 判断是否与指定的标示(当前线程标示)一致
- 如果一致则释放锁(删除)
- 如果不一致则什么都不做
unlock.lua(这个文件放在mian/resource下面)
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
Java代码如下
package cn.sticki.common.redis.utils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/** * @author 阿杆 * @version 1.0 * @date 2022/6/21 21:57 */
public class RedisSimpleLock implements ILock {
private final static String KEY_PREFIX = "lock:";
private final static String KEY_UUID = UUID.randomUUID() + ":";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
// 加载lua文件
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
private final StringRedisTemplate stringRedisTemplate;
private final String name;
public RedisSimpleLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeout) {
// 1. 生成key,通过拼接前缀和业务名
String key = KEY_PREFIX + name;
// 2. 生成value,用于判断该锁是不是当前线程生成的。使用随机的UUID+当前线程id,防止集群时value碰撞。
String value = KEY_UUID + Thread.currentThread().getId();
// 3. 尝试写入redis
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
// 4. 返回写入情况,成功即获取到锁
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
String key = KEY_PREFIX + name;
String value = KEY_UUID + Thread.currentThread().getId();
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(key),
value);
}
}
总结
实现思路:
- 利用set nx ex获取锁,并设置过期时间,保存线程标示
- 释放锁时先判断线程标示是否与自己一致,一致则删除锁
- 利用set nx满足互斥性
- 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
- 利用Redis集群保证高可用和高并发特性
可改进点:
- 不可重入:同一个线程无法多次获取同一把锁
- 不可重试:获取锁只尝试一次就返回false,没有重试机制
- 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
- 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁失效
引入Redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官网地址: https://redisson.org
GitHub地址: https://github.com/redisson/redisson
后记
这篇文章我是学习自黑马程序员的视频课程的时候做的总结和笔记,有兴趣的同学可以自行观看视频:https://www.bilibili.com/video/BV1cr4y1671t?p=56
边栏推荐
- OPPO VOOC快充电路和协议
- C语言函数之可变参数原理:va_start、va_arg及va_end
- Use of lists
- open-mmlab labelImg mmdetection
- Internet protocol details
- 几个关于指针的声明【C语言】
- E-commerce data analysis -- salary prediction (linear regression)
- [esp32 learning-1] construction of Arduino esp32 development environment
- MySQL realizes read-write separation
- Mp3mini playback module Arduino < dfrobotdfplayermini H> function explanation
猜你喜欢
FTP file upload file implementation, regularly scan folders to upload files in the specified format to the server, C language to realize FTP file upload details and code case implementation
Fashion Gen: the general fashion dataset and challenge paper interpretation & dataset introduction
JS object and event learning notes
ESP学习问题记录
Amba, ahb, APB, Axi Understanding
几个关于指针的声明【C语言】
Several declarations about pointers [C language]
RT-Thread API参考手册
STM32 how to locate the code segment that causes hard fault
Detailed explanation of 5g working principle (explanation & illustration)
随机推荐
ES6语法总结--下篇(进阶篇 ES6~ES11)
C language callback function [C language]
R & D thinking 01 ----- classic of embedded intelligent product development process
基于Redis的分布式ID生成器
Implementation scheme of distributed transaction
Correspondence between STM32 model and contex M
Bubble sort [C language]
FreeRTOS 任务函数里面的死循环
RT-Thread API参考手册
Principle and implementation of MySQL master-slave replication
嵌入式启动流程
History object
js 变量作用域和函数的学习笔记
uCOS-III 的特点、任务状态、启动
Selective sorting and bubble sorting [C language]
Reno7 60W超级闪充充电架构
【ESP32学习-2】esp32地址映射
列表的使用
Fashion Gen: the general fashion dataset and challenge paper interpretation & dataset introduction
Inline detailed explanation [C language]