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;

相关推荐
夜泉_ly2 小时前
MySQL -安装与初识
数据库·mysql
power-辰南3 小时前
高并发系统架构设计全链路指南
分布式·系统架构·高并发·springcloud
qq_529835353 小时前
对计算机中缓存的理解和使用Redis作为缓存
数据库·redis·缓存
月光水岸New5 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6755 小时前
数据库基础1
数据库
我爱松子鱼5 小时前
mysql之规则优化器RBO
数据库·mysql
chengooooooo6 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser7 小时前
【SQL】多表查询案例
数据库·sql
Galeoto7 小时前
how to export a table in sqlite, and import into another
数据库·sqlite
希忘auto7 小时前
详解Redis在Centos上的安装
redis·centos