好的,各位程序猿们,攻城狮们,以及未来将要加入我们行列的准码农们,晚上好!欢迎来到今晚的“Redis分布式锁:一场关于原子性的浪漫邂逅”讲座!
今天咱们不谈枯燥的理论,只聊实战,用最接地气的方式,揭开Redis分布式锁的神秘面纱,尤其是SET NX PX
命令和Lua脚本这两大利器的爱恨情仇。准备好了吗?让我们开始这场代码与艺术的碰撞之旅吧!🚀
第一幕:锁的江湖,风起云涌
在单机时代,线程锁就能搞定一切。那时候的日子,简单而美好,就像初恋,甜甜蜜蜜,毫无压力。 可是,随着互联网的飞速发展,我们的应用也变得越来越庞大,单机已经无法满足日益增长的业务需求。于是,我们不得不走向分布式架构。
分布式架构就像一个复杂的多人游戏,不同的服务器就像不同的玩家,都需要争夺共享资源。如果没有一个统一的规则,那就会乱成一锅粥,数据错乱,业务崩溃,简直就是一场灾难!
这个时候,分布式锁就应运而生,它就像一个公正的裁判,确保同一时刻只有一个玩家能够访问共享资源,从而保证数据的一致性和正确性。
第二幕:Redis登场,自带光环
在众多分布式锁的实现方案中,Redis凭借其高性能、高可用、易于部署等优点,成为了众多开发者的首选。它就像一位身披战甲的骑士,英勇地守护着我们的数据安全。
为什么Redis如此受欢迎呢?原因很简单:
- 高性能: Redis基于内存操作,速度极快,能够快速获取和释放锁,极大地降低了锁的竞争开销。
- 原子性: Redis的单线程架构保证了命令执行的原子性,避免了并发问题。
- 简单易用: Redis提供了简单易懂的API,方便开发者快速实现分布式锁。
第三幕:SET NX PX
:简单粗暴的“抢锁”姿势
SET NX PX
命令是Redis 2.6.12版本之后新增的,它提供了一种更加简洁高效的原子性操作,用于设置键值对,并同时设置过期时间。 它的语法如下:
SET key value [NX|XX] [EX|PX] [time]
其中:
key
:要设置的键名。value
:要设置的键值。NX
:仅当键不存在时才设置键值。XX
:仅当键存在时才设置键值。EX
:设置键的过期时间,单位为秒。PX
:设置键的过期时间,单位为毫秒。time
:过期时间。
我们可以利用 SET NX PX
命令来实现一个简单的分布式锁:
- 尝试获取锁: 使用
SET key value NX PX timeout
命令尝试设置一个键,如果设置成功,则表示获取锁成功;否则,表示获取锁失败。 - 释放锁: 使用
DEL key
命令删除该键,释放锁。
举个例子,假设我们要保护的共享资源是商品库存,我们可以使用如下代码:
// 尝试获取锁,key为商品ID,value为随机生成的UUID,防止误删锁,过期时间为10秒
String lockKey = "product_stock_" + productId;
String lockValue = UUID.randomUUID().toString();
String result = jedis.set(lockKey, lockValue, "NX", "PX", 10000);
if ("OK".equals(result)) {
// 获取锁成功,执行业务逻辑
try {
// 减少库存
decreaseStock(productId);
} finally {
// 释放锁
if (lockValue.equals(jedis.get(lockKey))) { // 再次确认是自己加的锁,防止误删
jedis.del(lockKey);
}
}
} else {
// 获取锁失败,稍后重试
Thread.sleep(100);
// 递归调用,或者使用循环重试
acquireLock();
}
这个代码看起来很简单,对不对?但是,它也存在一些问题:
- 删除锁的逻辑不够严谨: 我们需要先判断锁的值是否是自己设置的,才能删除锁,防止误删其他客户端的锁。
- 重试机制不够优雅: 获取锁失败后,我们只是简单地sleep一段时间后重试,这种方式效率较低,并且可能造成线程阻塞。
- 缺少自动续租机制: 如果业务逻辑执行时间超过了锁的过期时间,锁会自动释放,导致其他客户端获取到锁,造成并发问题。
总的来说,SET NX PX
命令就像一个愣头青,简单粗暴,虽然能够解决一些问题,但是也存在不少缺陷。
第四幕:Lua脚本:优雅而强大的“锁匠”
为了解决 SET NX PX
命令的缺陷,我们可以使用Lua脚本来实现更加完善的分布式锁。Lua脚本是一种轻量级的脚本语言,它可以嵌入到其他应用程序中使用。Redis支持执行Lua脚本,并且保证脚本执行的原子性。
使用Lua脚本,我们可以将获取锁、判断锁的值、删除锁等操作放在一个原子操作中执行,从而避免并发问题。
下面是一个使用Lua脚本实现分布式锁的示例:
-- 获取锁的脚本
local key = KEYS[1]
local value = ARGV[1]
local expireTime = ARGV[2]
if redis.call("SETNX", key, value) == 1 then
redis.call("PEXPIRE", key, expireTime)
return 1
else
return 0
end
这个脚本首先尝试使用 SETNX
命令设置键值对,如果设置成功,则使用 PEXPIRE
命令设置过期时间,并返回 1;否则,返回 0。
释放锁的脚本如下:
-- 释放锁的脚本
local key = KEYS[1]
local value = ARGV[1]
if redis.call("GET", key) == value then
return redis.call("DEL", key)
else
return 0
end
这个脚本首先判断锁的值是否是自己设置的,如果是,则使用 DEL
命令删除该键,并返回 1;否则,返回 0。
在Java代码中,我们可以这样使用Lua脚本:
// 获取锁的脚本
String acquireLockScript = "local key = KEYS[1]n" +
"local value = ARGV[1]n" +
"local expireTime = ARGV[2]n" +
"n" +
"if redis.call("SETNX", key, value) == 1 thenn" +
" redis.call("PEXPIRE", key, expireTime)n" +
" return 1n" +
"elsen" +
" return 0n" +
"end";
// 释放锁的脚本
String releaseLockScript = "local key = KEYS[1]n" +
"local value = ARGV[1]n" +
"n" +
"if redis.call("GET", key) == value thenn" +
" return redis.call("DEL", key)n" +
"elsen" +
" return 0n" +
"end";
// 加载脚本
String acquireLockSha = jedis.scriptLoad(acquireLockScript);
String releaseLockSha = jedis.scriptLoad(releaseLockScript);
// 尝试获取锁
Object result = jedis.evalsha(acquireLockSha, 1, lockKey, lockValue, String.valueOf(10000));
if (result.equals(1L)) {
// 获取锁成功,执行业务逻辑
try {
// 减少库存
decreaseStock(productId);
} finally {
// 释放锁
jedis.evalsha(releaseLockSha, 1, lockKey, lockValue);
}
} else {
// 获取锁失败,稍后重试
Thread.sleep(100);
// 递归调用,或者使用循环重试
acquireLock();
}
使用Lua脚本,我们可以将获取锁和释放锁的逻辑封装在一个原子操作中,避免了并发问题。同时,Lua脚本还提供了更强的灵活性,我们可以根据实际需求定制更加复杂的锁逻辑。
第五幕:自动续租:让锁更持久
前面我们提到,如果业务逻辑执行时间超过了锁的过期时间,锁会自动释放,导致其他客户端获取到锁,造成并发问题。为了解决这个问题,我们可以引入自动续租机制。
自动续租的原理很简单:在锁即将过期的时候,自动延长锁的过期时间,从而保证锁的持久性。
实现自动续租,我们可以使用一个后台线程,定时检查锁的过期时间,如果锁即将过期,则自动延长锁的过期时间。
下面是一个简单的自动续租的示例:
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// 启动自动续租
public void startAutoRenew() {
scheduler.scheduleAtFixedRate(() -> {
// 判断锁是否存在,并且锁的值是自己设置的
if (jedis.exists(lockKey) && lockValue.equals(jedis.get(lockKey))) {
// 延长锁的过期时间
jedis.pexpire(lockKey, 10000);
System.out.println("续租成功!");
} else {
// 锁已经被释放,停止续租
System.out.println("锁已释放,停止续租!");
scheduler.shutdown();
}
}, 5, 5, TimeUnit.SECONDS); // 每隔5秒检查一次,如果锁还有5秒过期,则续租
}
// 停止自动续租
public void stopAutoRenew() {
scheduler.shutdown();
}
这个代码使用 ScheduledExecutorService
定时检查锁的过期时间,如果锁即将过期,则使用 pexpire
命令延长锁的过期时间。
第六幕:Lua脚本 + 自动续租:终极解决方案
为了实现更加完善的分布式锁,我们可以将Lua脚本和自动续租机制结合起来。
我们可以使用Lua脚本来实现自动续租的逻辑,从而避免并发问题。
下面是一个使用Lua脚本实现自动续租的示例:
-- 续租的脚本
local key = KEYS[1]
local value = ARGV[1]
local expireTime = ARGV[2]
if redis.call("GET", key) == value then
redis.call("PEXPIRE", key, expireTime)
return 1
else
return 0
end
这个脚本首先判断锁的值是否是自己设置的,如果是,则使用 PEXPIRE
命令延长锁的过期时间,并返回 1;否则,返回 0。
在Java代码中,我们可以这样使用Lua脚本:
// 续租的脚本
String renewLockScript = "local key = KEYS[1]n" +
"local value = ARGV[1]n" +
"local expireTime = ARGV[2]n" +
"n" +
"if redis.call("GET", key) == value thenn" +
" redis.call("PEXPIRE", key, expireTime)n" +
" return 1n" +
"elsen" +
" return 0n" +
"end";
// 加载脚本
String renewLockSha = jedis.scriptLoad(renewLockScript);
// 启动自动续租
public void startAutoRenew() {
scheduler.scheduleAtFixedRate(() -> {
// 续租
Object result = jedis.evalsha(renewLockSha, 1, lockKey, lockValue, String.valueOf(10000));
if (result.equals(1L)) {
System.out.println("续租成功!");
} else {
// 锁已经被释放,停止续租
System.out.println("锁已释放,停止续租!");
scheduler.shutdown();
}
}, 5, 5, TimeUnit.SECONDS); // 每隔5秒检查一次,如果锁还有5秒过期,则续租
}
第七幕:总结:选择最适合你的“锁”
好了,经过一番讲解,相信大家对Redis分布式锁的实现细节已经有了更深入的了解。
特性 | SET NX PX |
Lua脚本 | Lua脚本 + 自动续租 |
---|---|---|---|
简单性 | 高 | 中 | 低 |
原子性 | 部分 | 高 | 高 |
可靠性 | 低 | 中 | 高 |
灵活性 | 低 | 中 | 高 |
适用场景 | 简单业务,对可靠性要求不高 | 中等复杂业务 | 复杂业务,对可靠性要求高 |
总的来说,SET NX PX
命令简单易用,适用于简单的业务场景;Lua脚本提供了更强的原子性和灵活性,适用于中等复杂的业务场景;Lua脚本 + 自动续租机制提供了最高的可靠性,适用于复杂的业务场景。
选择哪种方案,需要根据实际业务需求进行权衡。记住,没有最好的方案,只有最适合你的方案!
最后,希望大家在实际工作中能够灵活运用这些技巧,写出更加健壮、可靠的分布式应用! 感谢大家的聆听! 👏