0. 单机的并发问题
以一个『流水号生成』的场景为例,普通的后台应用通常都是使用时间戳的方式生成流水号,但是在用户量非常大的情况下,可能会出现并发问题。例如如下代码所示:
@Test
public void x() {
final CountDownLatch down = new CountDownLatch(1);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
down.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS");
String orderNo = sdf.format(new Date());
System.out.println("生成的订单号是 : " + orderNo);
}).start();
}
down.countDown();
}
运行以上代码,你会发现生成的 10 个订单号中,有不少是重复的。究其原因,就是因为在没有进行同步的情况下,出现了并发问题。
1. 分布式锁使用场景
一般我们使用分布式锁有两个场景:
效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。
正确性:加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。
Redis 因为其性能好,实现起来分布式锁简单,所以让很多人都对基于 Redis 实现的分布式锁十分青睐。
::: tip 提示
除了能使用 Redis 实现分布式锁之外,Zookeeper 也能实现分布式锁。但是项目中不可能仅仅为了实现分布式锁而专门引入 Zookeeper ,所以,除非你的项目体系中本来就有 Zookeeper<small>(来实现其它功能)</small>,否则不会单独因为分布式锁而引入它。
:::
2. SETNX 命令
早期,SETNX 是独立于 SET 命令之外的另一条命令。它的意思是 SET if Not eXists,即,在键值对不存在的时候才能设值成功。
::: warning 注意
SETNX 命令的价值在于:它将 判断
和 设值
两个操作合二为一,从而避免了 查查改改
的情况的出现。
:::
后来,在 Redis 2013 年推出的 2.6.12 版本中,Redis 为 SET 命令官方提供了 NX 选项,是的 SET 命令也能实现 SETNX 命令的功能。其语法如下:
SET <key> <value> [EX seconds] [PX milliseconds] [NX | XX]
EX 值的是 key
的存活时间,单位为秒。PX 与 EX 作用一样,唯一的不同就是后者的单位是微秒<small>(使用较少)</small>。
NX 和 XX 作用是相反的。NX 表示只有当 key『不存在时』才会设置其值;XX 表示当 key
存在时才设置 key
的值。
在 “升级” 了 SET 命令之后,Redis 官方说:“由于 SET 命令选项可以替换 SETNX,SETEX,PSETEX,因此在 Redis 的将来版本中,这三个命令可能会被弃用并最终删除”。
所以,现在我们口头所说的 SETNX 命令,并非单指 SETNX 命令,而是包括带 NX 选项的 SET 命令<small>(甚至以后就没有 SETNX 命令了)</small>。
3. SETNX 的使用
在使用 SETNX
操作实现分布式锁功能时,需要注意以下几点:
这里的『锁』指的是 Redis 中的一个认为约定的键值对。谁能创建这个键值对,就意味着谁拥有这整个『锁』。
-
使用
SETNX
命令获取『锁』时,如果操作返回结果是 0<small>(表示 key 已存在,设值失败)</small>,则意味着获取『锁』失败<small>(该锁被其它线程先获取)</small>,反之,则设值成功,表示获取『锁』成功。如果这个 key 不存在,SETNX 才会设置该 key 的值。此时 Redis 返回 1 。
如果这个 key 存在,SETNX 则不会设置该 key 的值。此时 Redis 返回 0 。
为了防止其它线程获得『锁』之后,有意或无意,长期持有『锁』而不释放<small>(导致其它线程无法获得该『锁』)</small>。因此,需要为 key 设置一个合理的过期时间。
当成功获得『锁』并成功完成响应操作之后,需要释放『锁』<small>(可以执行 DEL 命令将『锁』删除)</small>。
在代码层面,与 Setnx 命令对应的接口是 ValueOperations 的 setIfAbsent 方法。
4. Redis SETNX 的问题
如果在代码中使用 Redis 的 SETNX 命令,那么使用逻辑的伪代码如下:
String uuid1 = ...;
// lock
set Test uuid1 NX PX 3000
try {
// biz handle....
} finally {
// unlock
String uuid2 = get Test;
if (uuid1.equals(uuid2) {
redisTool.del('Test');
}
}
上面的代码逻辑有 2 个小问题:
-
上锁时,设置的超时自动删除时长<small>(3 秒)</small>,设置多长合适?万一设置短了怎么办?
如果设置短了,在业务逻辑执行完之前时间到期,那么 Redis 自动就把键值对给删除了,即,把锁给释放了,这不符合逻辑。
解锁时,
查 - 删
操作是 2 个操作,由两个命令完成,非原子性。
当然,上述两个问题我们都能解决点,不过有人<small>( Redisson )</small>帮我们把这些事情做好了。