1、为什么要使用分布式锁
如果在一个分布式系统中,我们从数据库中读取一个数据,然后修改保存,这种情况很容易遇到并发问题。因为读取和更新保存不是一个原子操作,在并发时就会导致数据的不正确。这种场景其实并不少见,比如电商秒杀活动,库存数量的更新就会遇到。如果是单机应用,直接使用本地锁就可以避免。如果是分布式应用,本地锁派不上用场,这时就需要引入分布式锁来解决。
由此可见分布式锁的目的其实很简单,就是为了保证多台服务器在执行某一段代码时保证只有一台服务器执行。
2、为了保证分布式锁的可用性,至少要确保锁的实现要同时满足以下几点:
- 互斥性。在任何时刻,保证只有一个客户端持有锁。
- 不能出现死锁。如果在一个客户端持有锁的期间,这个客户端崩溃了,也要保证后续的其他客户端可以上锁。
- 保证上锁和解锁都是同一个客户端。
3、一般来说,实现分布式锁的方式有以下几种:
- 使用MySQL,基于唯一索引。
- 使用ZooKeeper,基于临时有序节点。
- 使用Redis,基于set命令(2.6.12 版本开始)。
本篇文章主要讲解Redis的实现方式。
4、用到的redis命令
锁的实现主要基于redis的SET命令(SET详细解释参考这里),我们来看SET的解释:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- 将字符串值 value 关联到 key 。
- 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。
- 对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
可选参数
从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
加锁:使用SET key value [PX milliseconds] [NX]命令,如果key不存在,设置value,并设置过期时间(加锁成功)。如果已经存在lock(也就是有客户端持有锁了),则设置失败(加锁失败)。
解锁:使用del命令,通过删除键值释放锁。释放锁之后,其他客户端可以通过set命令进行加锁。
5、上面第二项,说了分布式锁,要考虑的问题,下面讲解一下
5.1、互斥性。在任何时刻,保证只有一个客户端持有锁
redis命令是原子性的,只要客户端调用redis的命令SET key value [PX milliseconds] [NX] 执行成功,就算加锁成功了
5.2、不能出现死锁。如果在一个客户端持有锁的期间,这个客户端崩溃了,也要保证后续的其他客户端可以上锁。
set命令px设置了过期时间,key过期失效了,就能避免死锁了
5.3保证上锁和解锁都是同一个客户端。
释放锁(删除key)的时候,只要确保是当前客户端设置的value才去删除key即可,采用lua脚本来实现
在Redis中,执行Lua语言是原子性,也就是说Redis执行Lua的时候是不会被中断的,具备原子性,这个特性有助于Redis对并发数据一致性的支持。
6、java代码实现
先把需要的jar包引入
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.3</version>
</dependency>