Redis 典型应用——分布式锁

一、什么是分布式锁

在一个分布式的系统中,也会涉及到多个节点访问同一个公共资源的情况,此时就需要通过锁来做互斥控制,避免出现类似于 "线程安全" 的问题;

而 Java 中的 synchronized,只能在当前进程中生效,在分布式的这种多个进程多个主机的场景下就无能为力了,此时就要使用到分布式锁;

考虑网购秒杀这样一个场景,限量 1 件商品,现有两个用户(两个客户端),在第一个用户访问一个服务器查询商品剩余量 > 0 之后,执行 -= 1 之前,第二个用户访问另一个服务器,查询此时商品剩余量仍然 > 0,也会执行 -= 1 操作,此时就会存在超卖问题(一个东西同时卖给两个用户);

在这个过程中,为了防止穿插执行的情况,可以使用 MySQL 的事务来解决,但如果使用的数据库不是 MySQL,没有事务,或者执行一段特定的操作,就不能使用 MySQL 的事务了;

此时就要使用分布式锁来解决上述问题,所谓的分布式锁,也是一个 / 一组单独的服务器程序,给其他服务器提供加锁这样的服务;Redis 就是一种典型的可以用来实现分布式锁的方案(mysql,zookeeper 也可以实现分布式锁的效果);

在购买过程前,需要下加锁,完成购买操作之后,再解锁,这样在购买期间,就不会被其他服务器穿插执行了;

二、Redis 实现分布式锁

1. setnx

Redis 实现分布式锁依靠 setnx 命令, setnx key value,如果 key 存在,就设置失败,否则设置 key value;

通过 setnx 实现加锁后,服务器执行相应逻辑之后,解锁就要通过 del 命令来删除 key;

2. set nx ex

如果某个服务器加锁成功后,执行相应逻辑时,服务器宕机,没有执行到解锁操作,就会导致其他服务器也无法获取到这个没有释放的锁(没有删除的 key);

因此就要为该 key 设置过期时间,一旦过期时间到了,就会自动删除该 key,可以使用 Redis 中的set nx ex 命令;

那能否使用 setnx 命令 + expire 设置过期时间呢?

答案是不能的,因为 Redis 中的多个命令之间是不能保证原子性的,不保证执行成功,因此使用一条命令实现是更稳妥的;

3. 引入校验 Id

加锁就是设置一个 key value,而解锁就是把相应的 key 删除;

那就可能存在服务器 1 执行了加锁操作,而服务器 2 执行了解锁操作;(误操作,bug)

这就需要引入校验机制,为每个服务器编号,在设置 key value 的时候,key 对应要对哪个资源加锁,value 就可以存储服务器的编号(标识出当前锁是哪个服务器加上的);

在解锁的时候,先判断该锁对应的编号是否为当前服务器的编号,如果是,才进行解锁操作;

4. 引入 lua 脚本

在上述解锁的过程中,需要先判定,再进行删除,这是两步操作(不是原子的),一个服务器内部,也可能是多线程的,若此时,该服务器内部有两个线程都在执行上述操作,就可能出现问题

此时可以使用 Redis 的事务解决该问题(避免插队),但实践中往往使用 lua 脚本来解决该问题;

lua 是一个特别轻量的编程语言,Lua 语法简单精炼,执行速度快,解释器也比较轻量,并且 Redis 本身就支持 Lua 作为内嵌脚本;因此可以使用 lua 编写一些逻辑,把这个脚本上传到 Redis 服务器上来执行,执行 lua 脚本的过程中是原子的 ;Redis 官方文档中也明确说到,lua是事务的替代方案;

if redis.call('get',KEYS[1]) == ARGV[1] then 
 return redis.call('del',KEYS[1]) 
else 
 return 0 
end;

上述代码可以编写成一个 .lua 后缀的⽂件,由 redis-cli 或者 jedis 等客户端加载,并发送给 Redis 服务器,由 Redis 服务器来执行这段逻辑,这样执行的效果就是原子的了;

5. 引入 watch dog

当我们为 key 设置了过期时间后,key 过期后会自动删除,但是此时任务还没执行完,这就会导致锁失效,其他服务器的任务来了就会重新设置 key;但如果将 key 的过期时间设置的太长,此时若服务器宕机,就会让其他服务器不能及时获取到锁;

因此就可以引入看门狗 ------ 加锁服务器上的一个单独的线程,通过这个线程来完成对锁的过期时间的续约;

比如,某个 key 的过期时间是 5 秒,同时看门狗线程会每 3 秒检测一次,当第一次经过 3 秒后,看门狗线程就会判定当前任务是否已经完成:

  • 如果任务未完成,则会将过期时间重新设置为 5 秒;
  • 如果任务已经完成,则通过执行上述 lua 脚本的方式删除 key;

这就解决了锁提前失效的问题;并且即使当加锁的服务器挂了,那么此时看门狗线程也会随之挂掉,此时就不会续约,这个 key 到过期时间了就自动删除了,不会影响其他的服务器;

三、总结

以上内容只是如何实现一个简单的互斥锁,在一些实际的应用场景中,我们可能会使用一些其他特殊的锁,比如可重入锁,公平锁,读写锁等等;

关于 Redis 实现分布式锁,在实践中,我们并不会真的自己实现一个分布式锁,已经有很多的库帮我们实现好了,直接使用即可,比较 Redisson;

相关推荐
Wang's Blog2 分钟前
Redis: 集群环境搭建,集群状态检查,分析主从日志,查看集群信息
数据库·redis
容器( ु⁎ᴗ_ᴗ⁎)ु.。oO23 分钟前
MySQL事务
数据库·mysql
cyt涛2 小时前
MyBatis 学习总结
数据库·sql·学习·mysql·mybatis·jdbc·lombok
Rookie也要加油3 小时前
01_SQLite
数据库·sqlite
liuxin334455663 小时前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
看山还是山,看水还是。3 小时前
MySQL 管理
数据库·笔记·mysql·adb
fishmemory7sec4 小时前
Koa2项目实战2(路由管理、项目结构优化)
数据库·mongodb·koa
momo小菜pa4 小时前
【MySQL 09】表的内外连接
数据库·mysql
Jasonakeke4 小时前
【重学 MySQL】四十九、阿里 MySQL 命名规范及 MySQL8 DDL 的原子化
数据库·mysql
程序猿小D4 小时前
第二百六十九节 JPA教程 - JPA查询OrderBy两个属性示例
java·开发语言·数据库·windows·jpa