分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
分布式锁的核心是实现多进程之间的互斥,而满足这一点的方式有很多,常见的有三种:
MySql | Redis | Zookeeper | |
---|---|---|---|
互斥 | 利用mysql本身的互斥锁机制 | 利用setnx这样的互斥命令 | 利用节点的唯一性和有序性实现互斥 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 好 | 一般 |
安全性 | 断开连接,自动释放锁 | 利用锁超时时间 | 临时节点,断开连接自动释放 |
实现分布式锁时需要实现的两个基本方法:
互斥:确保只有一个线程获取锁
非阻塞式:尝试一次,成功返回true,失败返回false
# 添加锁 利用setnx的互斥特性
SETNX lock thread1
# 添加锁过期的时间,避免服务宕机引起的死锁
EXPIRE lock 5
# 添加锁,NX是互斥,EX是设置超时时间 (使其具有了原子性)(最终命令)
SET lock thread1 NX EX 10
手动释放:
超时释放:获取锁时添加一个超时时间
# 释放锁
DEL key
大致流程:
example:基于Redis实现分布式锁初级版本
需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能。
类:
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(long timeoutSec){
// 获取线程标识
long threadId = Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX,threadId,timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock(){
// 释放锁
stringRedisTemplate.delete( KEY_PREFIX + name);
}
}
*** 改进Redis的分布式锁***:
需求:修改之前的分布式锁实现,满足︰
1.在获取锁时存入线程标示(可以用UUID表示)
2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";
@Override
public boolean tryLock(long timeoutSec){
// 获取线程标识
String threadId = ID_PREFIX+Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX,threadId,timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock(){
// 获取线程标示
String threadId = ID_PREFIX+Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标识是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
并发问题2:
解决此问题,需要使判断锁标识和释放锁操作为一个原子性操作!
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')
# 执行set name jack
local name = redis.call('set','name','jack')
# 返回
return name
写好脚本以后,需要使用Redis命令来调用脚本,调用脚本的常见命令如下(scription表示脚本的意思):
例如,我们要执行redis.call(‘set’ , ‘name’ , ‘jack’)这个脚本,语法如下:
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入keys数组,其他参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
Lua脚本语言的数组下标是从1开始的。
Redis的业务流程:
如果用Lua脚本来表示是这样的:
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程表示
-- 获取锁中的线程标示 get key
lcoal id = redis.call('get',KEYS[1])
-- 比较线程标示与锁中的标示是否一致
if(id == ARGV[1]) then
-- 释放锁 del key
return redis.call('del',KEYS[1])
end
return 0
需求:基于Lua脚本实现分布式锁的释放逻辑
提示:RedisTemplate调用Lua脚本的API如下:
特性:
基于setnx实现的分布式锁存在下面的问题:
不可重入:同一个线程无法多次获取同一把锁
不可重试:获取锁只尝试一次就返回false,没有重试机制
超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官网地址:https://redisson.org
GitHub地址:https://github.com/redisson/redisson
<dependency>
<groupid>org.redisson</groupid>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
<dependency>
@Configuration
public class Redisconfig {
@Bean
public RedissonClient redissonclient() {
//配置类
Config config = new Config();
//添加redis地址,这里添加了单点的地址,也可以使用config.useclusterServers ()添加集群
config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassowrd("123321");
//创建客户端
return Redisson.create(config);
}
@Resource
private RedissonClient redissonclient;
@Test
void testRedisson() throws InterruptedException {
//获取锁(可重入),指定锁的名称
RLock lock = redissonclient.getLock( "anyLock");
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//判断释放获取成功
if(isLock)
try{
system.out.println("执行业务");
}
finally{
//释放锁
lock.unlock();
}
}
}
总结: