Redis分布式锁-RedLock算法
手写分布式锁的缺点
Redlock算法设计理念
Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。
锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。
Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。
设计理念
该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。
假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,
为了获取锁,客户端会执行以下操作:
1 | 获取当前时间,以毫秒为单位; |
---|---|
2 | 依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁; |
3 | 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功; |
4 | 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。 |
5 | If the client failed to acquire the lock for some reason (either it was not able to lock N/2+1 instances or the validity time is negative), it will try to unlock all the instances (even the instances it believed it was not able to lock). |
容错公式
N=2X+1
N表示总机器数量,X表示宕机了几台。
部署N台可以保证宕机X台的情况客户端还能够正常获得锁。
RedLock算法的Java实现:Redisson
POM
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
RedisConfig
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.200.100:6379").setDatabase(0).setPassword("123456");
return (Redisson) Redisson.create(config);
}
Service
//V9.0
public String saleByRedisson() {
String retMessage = "";
RLock redissonLock = redisson.getLock("RedisLock");
redissonLock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
Integer inventorynumber = result == null ? 0 : Integer.parseInt(result);
if (inventorynumber > 0) {
//有库存
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventorynumber));
retMessage = "恭喜你,成功卖出一个商品,库存剩余:" + inventorynumber;
System.out.println(retMessage + "\t" + "服务端口号:" + port);
} else {
retMessage = "库存不足";
}
} finally {
redissonLock.unlock();
}
return retMessage + "\t" + "服务端口号:" + port;
}
Controller
@ApiOperation("扣减库存,一次卖一个Redisson")
@GetMapping(value = "/inventory/saleByRedisson")
public String saleByRedisson()
{
return inventoryService.saleByRedisson();
}
Jmeter压测没问题
不能误删别人的锁!
if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()){
redissonLock.unlock();
}
Redisson源码解析
额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。
Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间;
在获取锁成功后,给锁加一个 watchdog,watchdog会起一个定时任务,在锁没有被释放且快要过期的时候会续期
这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,优点在于 防止了 单节点故障造成整个服务停止运行的情况且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法。
Redisson 分布式锁支持 MultiLock 机制可以将多个锁合并为一个大锁,对一个大锁进行统一的申请加锁以及释放锁。
最低保证分布式锁的有效性及安全性的要求如下:
1.互斥;任何时刻只能有一个client获取锁
2.释放死锁;即使锁定资源的服务崩溃或者分区,仍然能释放锁
3.容错性;只要多数redis节点(一半以上)在使用,client就可以获取和释放锁
网上讲的基于故障转移实现的redis主从无法真正实现Redlock:
因为redis在进行主从复制时是异步完成的,比如在clientA获取锁后,主redis复制数据到从redis过程中崩溃了,导致没有复制到从redis中,然后从redis选举出一个升级为主redis,造成新的主redis没有clientA 设置的锁,这是clientB尝试获取锁,并且能够成功获取锁,导致互斥失效;
多机案例
Docker建3个Redis实例
docker run -p 6381:6379 --name redis-master-1 -d redis
docker run -p 6382:6379 --name redis-master-2 -d redis
docker run -p 6383:6379 --name redis-master-3 -d redis
docker exec -it redis-master-1 /bin/bash
docker exec -it redis-master-2 /bin/bash
docker exec -it redis-master-3 /bin/bash
POM
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.1</version>
</dependency>
坑爹的BUG
Docker创建容器默认都只有ipv6连接,所以在使用桥接模式的时候远程主机连接就会出现问题。
- ipv6可以兼容ipv4,但是ipv4不可以兼容ipv6
这时候我们就要手动设置ipv4的连接。
- 数据包转发 net.ipv4.ip_forward
- 当主机拥有多于一块的网卡时,其中一块收到数据包,根据数据包的目的ip地址将数据包发往本机另一块网卡,该网卡根据路由表继续发送数据包。这通常是路由器所要实现的功能;
- 执行
/sbin/sysctl net.ipv4.ip_forward
查看: 是否打开了数据包转发功能。 - 如果是0,就需要打开,使用命令
sysctl -w net.ipv4.ip_forward=1
即可。 - 重启Docker容器就可以了。
永久修改 net.ipv4.ip_forward
- 用
vim
修改文件/etc/sysctl.conf
net.ipv4.ip_forward = 1
- 保存后调用
sysctl -p
生效, ok问题解决;