手写分布式锁
qa

redis除了做缓存,还有什么用法


redis 单机与集群的cap分析

锁的种类
一个分布式锁需要满足的条件和刚需
- 独占性:任何时间只能有一个线程占有
- 高可用:
- 在redis集群环境下,不能因为一个节点挂了而出现获取锁和释放锁失败的情况
- 高并发请求下,依旧性能 ok
- 防死锁:杜绝死锁,必须有超时控制机制与撤销操作,有个兜底终止跳出方案
- 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放,自己约的锁含着泪也要自己解
- 重入性:同一个节点的同一个线程,获得锁之后,他也可以再次获取这个锁
分布式锁及其重点


setnx 来获取锁

boot+redis 基础案例
使用场景

java
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String retMessage = "";
lock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null? 0 : Integer.parseInt(result);
//3 扣除库存,每次减少一个
if (inventoryNumber > 0) {
inventoryNumber--;
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(inventoryNumber));
retMessage = "成功卖出一个商品,剩余库存" + inventoryNumber;
System.out.println(retMessage + ",端口号:" + port);
}else {
retMessage = "商品卖完了";
}
} finally {
lock.unlock();
}
return retMessage;
}
}
java
@RestController
@Api(tags = "redis 分布式锁测试")
public class InventoryController {
@Autowired
private InventoryService inventoryService;
@ApiOperation("扣除库存,一次卖一个")
@GetMapping("/inventory/sale")
public String sale() {
return inventoryService.sale();
}
}
GET http://localhost:6000/inventory/sale
手写分布式锁分析


拷贝出来一份
nginx 负载均衡
nginx
upstream stock {
server 127.0.0.1:6001 weight=1;
server 127.0.0.1:6000 weight=1;
}
server {
listen 9000;
server_name localhost;
location / {
proxy_pass http://stock;
index index.html index.htm;
}
}
http
GET http://localhost:9000/inventory/sale
jmeter
添加线程组 100*1

添加取样器-http请求

添加 listener-聚合报告

jmeter 100个线程1s执行完毕
执行完毕查看 库存数

为什么加了synchronized 和 lock 但是没有控制住




3.1 版本 setnx
java
public String sale() {
String retMessage = "";
String key = "zzyRedisLock";
String uuidValue = IdUtil.simpleUUID() + Thread.currentThread().getId();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
if (!flag) {
// 暂停20s,进行递归重试
try {TimeUnit.MICROSECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
sale();
} else {
// 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null? 0 : Integer.parseInt(result);
//3 扣除库存,每次减少一个
if (inventoryNumber > 0) {
inventoryNumber--;
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(inventoryNumber));
retMessage = "成功卖出一个商品,剩余库存" + inventoryNumber;
System.out.println(retMessage + ",端口号:" + port);
}else {
retMessage = "商品卖完了";
}
} finally {
stringRedisTemplate.delete(key);
}
}
return retMessage + ",端口号:" + port;
}

3.2 自旋代替递归重试 while替代if
java
public String sale() {
String retMessage = "";
String key = "zzyRedisLock";
String uuidValue = IdUtil.simpleUUID() + Thread.currentThread().getId();
// 不用递归了,高并发下使用自旋替代递归重试,使用while
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)) {
try {
TimeUnit.MICROSECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣除库存,每次减少一个
if (inventoryNumber > 0) {
inventoryNumber--;
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(inventoryNumber));
retMessage = "成功卖出一个商品,剩余库存" + inventoryNumber;
System.out.println(retMessage + ",端口号:" + port);
} else {
retMessage = "商品卖完了";
}
} finally {
stringRedisTemplate.delete(key);
}
return retMessage + ",端口号:" + port;
}
部署了微服务的Java程序挂了,代码层面根本没有走到 finally 这块,没办法保证解锁(无过期时间该key一直存在),这个key没有被删除,需要加入一个过期时间限定 key
4.1 setnx 添加过期时间

4.2 加锁和过期时间必须在同一行,保证原子性

5.0 防止误删key
锁超时误删除


6.0 lua 脚本实现删除时判断和删除操作的原子性

介绍 lua 脚本
调用 lua 脚本,教程
- 实现 hello lua
注意 0 作为唤醒词

- 实现 set expire get
redis.call 用来调用命令,最后一个需要返回使用 return


- mset 重在掌握 lua脚本中 args传参的用法
2 -> key argv 数量
k1 k2 -> key1 key2 前两个 key,后面都是 argv
lua1 lua2 -> argv1 argv2

- 官网lua脚本 如何执行呢
lua
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

补充说明 if elseif else


java
// 改进点,修改为 Lua 脚本的 redis 分布式锁调用,必须保证原子性,参考官网脚本案例
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript(luaScript, Boolean.class), Arrays.asList(key), uuidValue);
new DefaultRedisScript(luaScript, Boolean.class)
->Boolean.class
指定返回值,不指定会报错

7.0 可重入 + 设计模式
juc 中可重入的解释

可重入锁的解释

juc 中可重入锁的案例演示(同步代码块,同步方法,Lock重点)

同步代码块

同步方法

注意使用 Lock 的时候,重入的时候加锁解锁次数要匹配,解锁次数不足则会导致锁仍然被占用,记得这里有个计数器



redis 中如何实现 aqs 中可重入规范

需要一个计数器,需要 kkv 这种结构才能满足 -> hset

实现思路分析


lua 脚本分析
加锁Lock
- 判断redis分布式锁key是否存在 EXISTS key
- 不存在,hset新建当前线程属于自己的锁 uuid:ThreadId
- 存在,说明已经有锁 HEXISTS key uuid:ThreadId
- 不是自己的
- 是自己的,自增一次
lua
-- 加锁的lua的脚本,对标我们的lock方法
-- 加锁 v1
if redis.call('exists', 'key') == 0 then
redis.call('hset', 'key', 'uuid:threadid', 1)
redis.call('expire', 'key', '50')
return 1
elseif redis.call('hexists', 'key', 'uuid:threadid') == 1 then
redis.call('hincrby', 'key', 'uuid:threadid', 1)
redis.call('expire', 'key', '50')
return 1
else
return 0
end
-- v2 合并相同的代码,用incrby替代hset 简化代码
if redis.call('exists', 'key') == 0 or redis.call('hexists', 'key', 'uuid:threadid') == 1 then
redis.call('hincrby', 'key', 'uuid:threadid', 1)
redis.call('expire', 'key', '50')
return 1
else
return 0
end
-- v3 脚本ok了, 替换参数
if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
redis.call('hincrby', KEY[1], ARGV[1], 1)
redis.call('expire', KEY[1], ARGV[2])
return 1
else
return 0
end
-- 合并到一行
if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then redis.call('hincrby', KEY[1], ARGV[1], 1) redis.call('expire', KEY[1], ARGV[2]) return 1 else return 0 end
加锁 unLock
设计思路:解锁,还得是自己的锁
lua
-- v1 解锁
if redis.call('hexists', 'key', 'uuid:threadid') == 0 then
return nil
elseif redis.call('hincrby', 'key', 'uuid:threadid', -1) == 0 then
return redis.call('del', 'key')
else
return 0
end
-- v2 替换下参数
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then
return nil
elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 then
return redis.call('del', KEYS[1])
else
return 0
end
给出测试代码
加锁四次,解锁四次
shell
EVAL "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 zzyyRedisLock 001:1 50
EVAL "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 zzyyRedisLock 001:1 50
EVAL "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 zzyyRedisLock 001:1 50
EVAL "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 zzyyRedisLock 001:1 50
ttl zzyyRedisLock
HGET zzyyRedisLock 001:1
EVAL "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then return nil elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 then return redis.call('del', KEYS[1]) else return 0 end" 1 zzyyRedisLock 001:1
EVAL "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then return nil elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 then return redis.call('del', KEYS[1]) else return 0 end" 1 zzyyRedisLock 001:1
EVAL "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then return nil elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 then return redis.call('del', KEYS[1]) else return 0 end" 1 zzyyRedisLock 001:1
EVAL "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then return nil elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 then return redis.call('del', KEYS[1]) else return 0 end" 1 zzyyRedisLock 001:1
lua脚本整合进程序->实现 Lock 接口

我们创建一个 Lock 实现类,重写lock(重载到 tryLock 中最终实现),unlock方法
java
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L) {
String luaScript = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 then " +
" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
" return 1 " +
"else" +
" return 0 " +
"end";
while (!(Boolean) stringRedisTemplate.execute(new DefaultRedisScript(luaScript, Boolean.class),
Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))){
try {
Thread.sleep(60);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
return true;
}
return false;
}
@Override
public void unlock() {
String luaScript = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then " +
" return nil " +
"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
// nil = false 1 = true 0 = false
// 这里返回值不方便使用 Boolean
Long flag = (Long) stringRedisTemplate.execute(new DefaultRedisScript(luaScript, Long.class),
Arrays.asList(lockName), uuidValue);
if (null == flag) {
throw new RuntimeException("锁不存在");
}
}
7.1 工厂模式优化,方便整合其他锁实现


锁的实现写死了,以后如果引入别的类型的锁,zookeeper 等
java
@Component
public class DistributedLockFactory {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
public Lock getDistributedLock(String lockType) {
if (lockType == null) {
return null;
}
if (lockType.equalsIgnoreCase("REDIS")) {
this.lockName = "zzyRedisLock";
return new RedisDistributedLock(stringRedisTemplate, lockName);
}else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
this.lockName = "zzyZookeeperLock";
// TODO zookeeper lock
return null;
} else if (lockType.equalsIgnoreCase("MYSQL")) {
// TODO mysql lock
}
return null;
}
}
7.2 可重入测试重点->解决uuid不一致,最终实现可重入


解决重入后 uuid 不一致问题
uuid 直接从工厂获取

这块说的比较碎,结合起来理解下
8.0 自动实现锁续期
CAP

Redis 可能存在异步复制导致的锁丢失,主节点还没来得及把刚刚set进来的这条数据同步到从节点,master就挂了,从机上位,但从机上无该数据

zookeeper 只有整个同步全部成功,才会注册成功,速度会稍慢。主节点挂了,只有选举出新老大后,才可用

euraka
加钟的 lua
在拿到锁的同时添加一个后台程序,为ttl 的 1/3
续期后再调用自己添加 监视程序,保证能够一直续期
lua
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
return redis.call('expire', KEYS[1], ARGV[1])
else
return 0
end
代码中实现->加一个延时定时器任务
递归调用,3/1 时间间隔,如果锁仍然持有的话
比如 expireTime = 30,第20秒触发,继续续到 30
java
// tryLock 返回之前
// 新建一个后台扫描程序,来监视key目前的ttl,是否到我们规定的 1/2 1/3 来实现续期
resetExpire();
public void resetExpire() {
String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then " +
"return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
"return 0 " +
"end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if ((boolean) stringRedisTemplate.execute(new DefaultRedisScript(script, Boolean.class),
Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
resetExpire();
}
}
}, (this.expireTime * 1000) / 3);
}
小总结&如果让你自研一把分布式锁,你要怎么做
- 按照 juc Lock 接口规范编写
- lock 加锁关键逻辑
- 加锁
- 给key设置一个值,为避免死锁,并给定一个过期时间
- 自旋
- 续期
- 加锁


- unlock 解锁关键逻辑
- 将 key 键删除,但也不能乱删,不能删除其他客户端的锁

redlock与 redisson
来由->我们手写的分布式锁有什么缺点
redis 单机故障


RedLock算法设计理念




容错公式

v9.0

Getting Started - Redisson Reference Guide
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.20.1</version>
</dependency>
java
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
RLock lock = redisson.getLock(key);
看门狗,续期
v9.1


Redisson 源码分析










多机案例->MultiLock 创建联锁


但 RedLock 被标记弃用

将多个 RLock 对象关联为一个联锁对象,只有所有节点加锁成功,联锁才成功
实操







我们创建三个 RedisClient,然后创建一个联锁即可
