Redis分布式锁核心原理:SET NX PX与Lua脚本原子性
很多刚接触分布式的同学,一听到“锁”就觉得很高大上,其实换个角度看,在Redis里实现分布式锁,核心就是抢座位。谁先抢到,谁就能干活,其他人只能等着或者滚蛋。
咱们先聊聊最基础的加锁。早期大家可能用过SETNX命令,意思是SET if Not eXists。这个命令确实能保证只有一个客户端能设置成功,但它有个致命伤:没法同时设置过期时间。如果你用SETNX抢到了锁,还没来得及执行EXPIRE设置超时时间,这时候你的服务直接宕机了,那这个锁就成“死锁”了,永远解不开。这在生产环境里就是个定时炸弹。
所以,Redis在2.6.12版本之后,官方推荐的做法是直接使用一条原子命令:SET key value NX PX timeout。
这一条命令干了三件事:
- NX:只有在key不存在时才设置(抢锁)。
- PX:设置过期时间(防止死锁)。
- 原子性:这两步是打包在一起执行的,中间不会被打断。
注意,千万不要分开执行SETNX和EXPIRE,这是新手最容易踩的坑。
那value值设什么呢?很多教程随便写个1,这不行。如果你设个固定的值,万一客户端A还没执行完,锁过期自动释放了,客户端B抢到了锁,这时候客户端A执行完了,去删锁,不就把客户端B的锁给干掉了吗?这就叫“误删”。
所以,加锁的时候,value必须是一个唯一标识,比如UUID或者UUID:线程ID。
import redis.clients.jedis.Jedis;
import java.util.Collections;
import java.util.UUID;
public class SimpleRedisLock {
private Jedis jedis;
private String lockKey;
public SimpleRedisLock(Jedis jedis, String lockKey) {
this.jedis = jedis;
this.lockKey = lockKey;
}
/**
* 尝试加锁
* @param requestId 唯一标识,可以用UUID
* @param expireTime 锁的过期时间,单位毫秒
* @return 是否加锁成功
*/
public boolean tryLock(String requestId, int expireTime) {
// 使用 SET 命令的 NX 和 PX 选项
// 这里的 "NX" 表示不存在才设置,"PX" 表示设置毫秒级过期时间
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}
/**
* 释放锁(错误示范,存在原子性问题)
* 这里先注释掉,为了引出下面的Lua脚本
*/
/*
public void unlockBad(String requestId) {
// 这种写法有大问题!查和删不是原子的
String value = jedis.get(lockKey);
if (requestId.equals(value)) {
// 这里如果锁刚好过期,别的线程已经加锁了,你这一删就出大事了
jedis.del(lockKey);
}
}
*/
/**
* 释放锁(正确姿势:使用Lua脚本保证原子性)
* @param requestId 加锁时用的唯一标识
* @return 是否释放成功
*/
public boolean unlock(String requestId) {
// Lua脚本:先判断value是不是自己的,是的话再删除
// 这样可以保证“查询”和“删除”这两个操作是原子性的
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Object result = jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return "1".equals(result.toString());
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String lockKey = "stock_lock";
String requestId = UUID.randomUUID().toString();
SimpleRedisLock lock = new SimpleRedisLock(jedis, lockKey);
try {
// 尝试加锁,设置10秒过期
if (lock.tryLock(requestId, 10000)) {
System.out.println("加锁成功,开始处理业务...");
// 模拟业务处理
Thread.sleep(3000);
System.out.println("业务处理完成。");
} else {
System.out.println("加锁失败,系统繁忙。");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
boolean released = lock.unlock(requestId);
System.out.println("锁释放状态: " + released);
}
jedis.close();
}
}
上面这个代码里,解锁部分我特意用了Lua脚本。为啥?因为Redis执行Lua脚本的时候,会被当成一条命令,中间不会被其他命令插队。这就解决了“我查到锁是我的,还没来得及删,锁过期被别人抢了,然后我把别人的锁删了”这种并发问题。
📌 要点提醒:如果你只是做简单的Demo或者非核心业务,用上面的SET NX PX配合Lua脚本解锁基本够用了。但你要记住,这里的锁过期时间很难设。设短了,业务没跑完锁就没了;设长了,服务宕机了恢复得等半天。这也是为啥后面我们要引入Redisson的看门狗机制。
---
Redis 7.x 实战:基于Redisson实现可重入锁与自动续期(看门狗)
刚才咱们手写的锁虽然能用,但在生产环境里,你还得考虑可重入、续期、阻塞等待这些问题,自己造轮子太累了,而且容易出Bug。这时候,就得请出Redis Java客户端里的“大哥大”——Redisson。
Redisson可不是简单的Jedis替代品,它实现了很多分布式Java对象和服务。对于分布式锁,它支持可重入、公平锁、读写锁,还有那个非常牛的Watch Dog(看门狗)机制。
咱们现在用的Redis都到7.2.5(2024年5月发布)了,Redisson的版本也一直在迭代,对Redis 7.x的支持非常完善。
换个角度看,Redisson的锁之所以牛,是因为它底层不是简单的SET NX,而是用了Hash结构。
- Key:就是你的锁名字。
- Field:是客户端ID + 线程ID(
UUID:ThreadID)。
- Value:是重入次数(默认从1开始)。
这样,同一个线程多次调用lock(),Value就加1,实现了可重入。解锁的时候减1,减到0才真正删除Key。
最爽的是看门狗(Watch Dog)。你加锁的时候如果不指定leaseTime(锁自动释放时间),Redisson默认会启动一个后台定时任务。这个任务每隔一段时间(默认是锁过期时间的1/3,比如你锁默认30秒,它每10秒检查一次)去续期。只要你的线程还活着,锁就一直不过期。除非你手动释放,或者整个JVM挂了,那锁才会等30秒后自动消失。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedissonLockDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 配置Redisson客户端,这里用单Redis节点做演示
// 如果是集群,改成 useClusterServers() 就行
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(5);
// 2. 创建客户端
RedissonClient redisson = Redisson.create(config);
// 3. 获取分布式锁对象
// 这里的 "order:lock:123" 就是锁的Key
RLock lock = redisson.getLock("order:lock:123");
try {
System.out.println(Thread.currentThread().getName() + " 尝试加锁...");
// 4. 加锁
// 这里不传过期时间,就是开启看门狗模式
// waitTime: 最多等待100秒获取锁
// leaseTime: 如果不传,默认-1,开启看门狗自动续期
boolean isLocked = lock.tryLock(100, TimeUnit.SECONDS);
if (isLocked) {
System.out.println(Thread.currentThread().getName() + " 加锁成功!开始处理订单...");
// 模拟业务耗时,这里故意搞个40秒,超过默认的30秒过期时间
// 如果是普通锁,早就过期被别人抢走了
// 但有了看门狗,它会自动续期,所以这里很安全
for (int i = 0; i < 4; i++) {
System.out.println("业务执行中... " + (i + 1) + "/4");
Thread.sleep(10000);
}
// 测试可重入性
reentrantTest(lock);
System.out.println("订单处理完成!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 5. 释放锁
// 一定要判断是不是还持有锁,防止重复释放报错
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 锁已释放.");
}
}
// 关闭客户端
redisson.shutdown();
}
public static void reentrantTest(RLock lock) {
// 同一个线程再次加锁,这就是可重入
lock.lock();
try {
System.out.println("进入可重入方法,锁重入次数增加。");
} finally {
lock.unlock(); // 这里解锁一次,重入次数减1
}
}
}
跑一下这个代码,你会发现,即便业务跑了40秒,锁依然稳稳地在你手里。这就是看门狗的威力。
📌 要点提醒:在实际开发中,用tryLock(waitTime, leaseTime, unit)的时候,如果你明确知道业务执行时间,可以指定leaseTime,这样Redisson就不会开启看门狗,减少一点开销。如果业务时间不确定,那就别传leaseTime,让它自动续期。另外,记得在finally块里判断lock.isHeldByCurrentThread(),避免因为业务异常导致没拿到锁却去释放锁的尴尬。
---
进阶:RedLock算法原理与集群高可用容错实现
前面咱们聊的Redisson锁,默认是基于单Master节点的。这在单机或者主从架构下没啥问题。但是,如果你用的是Redis Cluster,或者你对高可用性要求极高,单节点挂了咋办?这就引出了RedLock(红锁)算法。
RedLock是Redis作者Antirez提出来的。打个比方,它的思路就是:我不指望一个Redis节点靠谱,我多找几个独立的节点,只要大多数节点(过半数)都抢锁成功,我就认为我拿到锁了。
具体流程是这样的(假设我们有5个完全独立的Redis Master节点,它们没有主从关系,也不互相通信):
- 客户端获取当前系统时间(毫秒级)。
- 客户端依次向这5个节点发起加锁请求(使用相同的Key和随机Value),并且设置一个远小于锁过期时间的超时时间(比如锁过期是10秒,超时设5-50毫秒)。这是为了防止在某个宕机节点上死等。
- 只要客户端在锁过期时间内,成功从大多数节点(>= N/2 + 1,也就是5个里至少3个)抢到了锁,并且总耗时小于锁过期时间,这把锁就算加成功了。
- 如果加锁失败(比如没拿到多数派同意,或者总耗时超过了锁有效期),客户端就会向所有节点发起解锁请求(哪怕那个节点它没加成功也要发,用Lua脚本解)。
RedLock确实提升了可靠性,但它在社区里争议也很大。特别是那个Martin Kleppmann,他吐槽说RedLock依赖系统时钟,如果系统时间跳变(比如运维手动改了时间),可能会导致锁失效。而且,在极端网络延迟下,也可能出问题。
即便如此,在很多对一致性要求没那么变态(不需要像银行转账那样强一致),但又想防止单点故障的场景下,RedLock还是很有用的。
Redisson已经帮我们实现了RedLock,用起来非常简单,你只需要把多个RedissonClient实例传进去就行。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedLockDemo {
public static void main(String[] args) throws InterruptedException {
// 模拟3个独立的Redis节点(实际生产环境应该是3台不同的机器或容器)
// 这里为了方便,我们连同一个实例的不同DB,或者你搭3个端口
// 注意:真实RedLock要求节点完全独立,不能共享持久化或主从关系
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 假设还有第二个节点
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://127.0.0.1:6380"); // 假设你有6380端口的实例
// 假设还有第三个节点
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://127.0.0.1:6381"); // 假设你有6381端口的实例
RedissonClient client1 = Redisson.create(config1);
RedissonClient client2 = Redisson.create(config2);
RedissonClient client3 = Redisson.create(config3);
// 1. 构造RedLock
// 把多个RLock实例传进去,这些实例必须来自不同的RedissonClient
RLock lock1 = client1.getLock("resource_lock");
RLock lock2 = client2.getLock("resource_lock");
RLock lock3 = client3.getLock("resource_lock");
// RedissonRedLock 是 RedLock 算法的实现
RLock redLock = new org.redisson.RedissonRedLock(lock1, lock2, lock3);
try {
System.out.println("尝试使用RedLock加锁...");
// 2. 加锁
// waitTime: 等待锁的时间
// leaseTime: 锁的过期时间,这里设置30秒
boolean isLocked = redLock.tryLock(100, 30, TimeUnit.SECONDS);
if (isLocked) {
System.out.println("RedLock加锁成功!开始执行关键业务...");
// 模拟业务
Thread.sleep(5000);
System.out.println("业务执行完毕。");
} else {
System.out.println("RedLock加锁失败,可能存在冲突。");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 3. 解锁
// RedLock的解锁会向所有节点发送解锁请求
if (redLock.isHeldByCurrentThread()) {
redLock.unlock();
System.out.println("RedLock已解锁。");
}
}
// 关闭客户端
client1.shutdown();
client2.shutdown();
client3.shutdown();
}
}
🔧 实战技巧:虽然RedLock听起来很美,但别盲目上。如果你的场景是秒杀库存扣减,用普通的单节点Redisson锁(配合高可用主从)通常就够了,没必要引入RedLock的复杂度。RedLock更适合那种集群定时任务调度,比如你要确保集群里10台机器,只有1台能跑那个定时任务,这时候用RedLock就很合适。另外,根据2024年的技术趋势,社区也在讨论未来Redis 8.0+可能会把锁做成原生模块,到时候可能就不用这么折腾客户端了。现在嘛,Redisson的RedissonRedLock依然是最稳的选择之一。
4. 场景实战:电商库存扣减与接口幂等性防重提交
换个角度看,学分布式锁不能光看理论,得看它怎么在真实业务里“救命”。在电商和支付系统里,分布式锁主要干两件事:一是防超卖(库存扣减),二是防重单(接口幂等)。这两个场景要是没处理好,老板分分钟让你背锅。
4.1 场景一:电商秒杀库存扣减
咱们先说库存扣减。假设现在有个秒杀活动,库存只剩1个,结果有100个请求同时进来。如果没有锁,这100个请求都读到库存是1,然后都去减1,结果库存直接变负数,这就是经典的“超卖”。
虽然Redis的DECR命令本身是原子的,但在实际业务中,我们往往需要先查库存、再扣库存,或者扣完还要写日志,这就不是单个命令能搞定的了。这时候就得上分布式锁。
这里我直接用 Redisson 来演示。为啥不用Jedis手写?换个角度看,Redisson已经帮我们封装好了看门狗(Watch Dog)机制,也就是自动续期。在Redis 7.x时代,Redisson依然是最稳的客户端之一,它解决了“业务没执行完锁就过期”的痛点。
下面是一段完整的Spring Boot风格代码,模拟扣减库存:
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class StockService {
private RedissonClient redisson;
private String stockKey = "product:1001:stock"; // 假设商品ID是1001
public StockService() {
// 1. 初始化Redisson客户端(这里以单机为例,Redis 7.2.5环境)
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
this.redisson = Redisson.create(config);
}
public boolean deductStock(String productId) {
// 2. 定义锁的key,通常是业务唯一标识
String lockKey = "lock:stock:" + productId;
RLock lock = redisson.getLock(lockKey);
try {
// 3. 尝试加锁
// 参数说明:
// waitTime: 最多等待5秒(如果拿不到锁就放弃)
// leaseTime: -1 代表不指定过期时间,交给Redisson的看门狗自动续期(默认30秒,每10秒续一次)
// 如果这里填了具体数字,比如 10,看门狗就失效了,锁10秒后强制过期
boolean isLocked = lock.tryLock(5, -1, TimeUnit.SECONDS);
if (!isLocked) {
System.out.println(Thread.currentThread().getName() + ":没抢到锁,请求太拥挤了!");
return false;
}
// 4. 核心业务逻辑:扣减库存
// 这里模拟从Redis获取库存并扣减
// 实际场景可能还要查数据库,这里为了演示简单,直接用Redis操作
Long stock = redisson.getAtomicLong(stockKey).get();
if (stock > 0) {
redisson.getAtomicLong(stockKey).decrementAndGet();
System.out.println(Thread.currentThread().getName() + ":扣减成功,剩余库存:" + (stock - 1));
// 模拟业务耗时
Thread.sleep(100);
return true;
} else {
System.out.println(Thread.currentThread().getName() + ":库存不足!");
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
// 5. 释放锁
// 核心要点:一定要判断锁是否被当前线程持有,以及是否处于锁定状态,防止报错
if (lock.isHeldByCurrentThread() && lock.isLocked()) {
lock.unlock();
System.out.println(Thread.currentThread().getName() + ":锁已释放");
}
}
}
}
⚡ 效率提示:在写库存扣减代码时,千万别把leaseTime随便设一个很短的时间(比如3秒)。我见过太多新手因为业务处理稍微慢了点,锁就自动释放了,结果后面的线程进来把数据搞乱了。直接用lock.tryLock(等待时间, -1, 单位)让看门狗帮你干活,省心。
4.2 场景二:订单接口幂等性防重提交
再来说说接口幂等。用户点击“提交订单”,结果因为网络卡顿,用户手抖连点了两次。如果不做处理,后端可能生成两笔一模一样的订单。这时候分布式锁就派上用场了,我们可以用“订单号”或者“用户ID+业务类型”作为锁的维度。
这里我们用原生Redis命令配合Lua脚本来实现,这样更轻量,适合不想引入重客户端(如Redisson)的场景。
import redis.clients.jedis.Jedis;
import java.util.Collections;
import java.util.UUID;
public class IdempotentService {
private Jedis jedis;
public IdempotentService() {
// 连接 Redis 7.2.5
this.jedis = new Jedis("127.0.0.1", 6379);
}
/**
* 模拟提交订单,利用分布式锁防重
*/
public String submitOrder(String userId, String orderId) {
String lockKey = "order:lock:" + userId + ":" + orderId;
// 生成唯一value,用于防止误删
String requestId = UUID.randomUUID().toString();
try {
// 1. 加锁:SET key value NX PX 30000
// NX表示不存在才设置,PX表示过期时间30秒
String result = jedis.set(lockKey, requestId, "NX", "PX", 30000);
if ("OK".equals(result)) {
// 2. 模拟业务处理:检查订单是否已存在,不存在则创建
System.out.println("处理订单逻辑,RequestId: " + requestId);
Thread.sleep(50); // 模拟耗时
return "订单提交成功";
} else {
return "请勿重复提交订单";
}
} catch (Exception e) {
e.printStackTrace();
return "系统异常";
} finally {
// 3. 解锁:使用Lua脚本保证原子性
// 脚本逻辑:如果value相等,说明是自己的锁,就删除;否则不操作
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
// 执行Lua脚本
jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(requestId));
}
}
}
💡 经验总结:手写解锁逻辑时,一定要用Lua脚本,千万别先get再del。简单来说,在高并发下,get到值之后,del之前,锁可能刚好过期被别人抢走了,你这一del就把别人的锁给删了,这就出大事故了。
---
5. :锁超时解决、Fencing Token与RedLock争议
做分布式锁这一块,坑是真的多。很多新手以为加个锁就万事大吉了,结果线上一跑,各种诡异问题。这一章咱们聊聊那些年我们踩过的坑,以及社区里吵得不可开交的RedLock争议。
5.1 锁超时了,业务还没跑完怎么办?
这是最经典的问题。比如你设置了锁10秒过期,结果业务代码里有个慢查询或者调了第三方接口,15秒才跑完。这时候锁早就自动释放了,其他线程已经进来了,你的业务跑完再去释放锁,可能就把别人的锁给干掉了。
解决方案有两个:
- 自动续期(Watch Dog):就像前面Redisson演示的那样,后台起个线程,每隔一段时间(比如过期时间的1/3)去检查锁还在不在,还在的话就延长过期时间。这是最省心的办法。
- 业务兜底:如果不用Redisson,自己写的话,可以在业务代码里捕获异常,或者把过期时间设得足够长(但这治标不治本)。
5.2 Fencing Token(栅栏令牌)
这个东西是分布式系统大神Martin Kleppmann提出来的,专门用来解决“客户端暂停(比如GC停顿)”导致锁失效的问题。
场景是这样的:
- 客户端A获取锁,锁过期时间是10秒。
- 客户端A由于Full GC,停顿了15秒。
- 锁自动过期,客户端B获取到了锁。
- 客户端A醒过来了,继续执行,它以为自己还持有锁,于是去写数据,把客户端B的数据覆盖了。
怎么解决?
引入一个单调递增的令牌(Fencing Token)。每次加锁成功,Redis不仅返回成功,还返回一个递增的Token(比如用INCR生成)。客户端在写存储(比如数据库或Zookeeper)的时候,必须带上这个Token。存储层会检查这个Token是不是比之前记录的Token大,如果不是,直接拒绝写入。
虽然Redis本身没有内置Fencing Token机制,但我们可以在业务层实现:
// 伪代码逻辑,展示Fencing Token思路
public void processWithFencing() {
// 1. 获取锁的同时,获取一个递增的Token
// 假设我们用一个独立的Key来维护Token计数器
long token = jedis.incr("global:fencing:token");
// 2. 加锁,把Token作为Value的一部分存进去
String lockValue = "client_id:" + token;
jedis.set("resource_lock", lockValue, "NX", "EX", 30);
// 3. 访问共享资源(比如数据库)
// 在数据库操作前,检查当前Token是否是最新的
// 这里需要数据库支持,比如加个version字段或者检查存储的token值
// if (database.getMaxToken() >= token) { throw new Exception("操作过时"); }
// 4. 执行业务...
}
5.3 RedLock争议:到底还能不能用?
说到RedLock,这可是分布式锁界的“罗生门”。RedLock是Redis作者Antirez基于多个独立Master节点设计的算法,目的是在集群环境下提高可靠性。
RedLock的逻辑:
客户端向N个(通常是5个)独立的Redis Master节点申请锁,如果超过半数(>= N/2 + 1)的节点加锁成功,且总耗时小于锁过期时间,才算加锁成功。
但是,Martin Kleppmann提出了质疑(这也是面试考点梳理):
- 依赖系统时钟:如果某个Redis节点发生了时钟跳跃(比如运维手动改了时间),可能导致锁提前过期,让多个客户端同时持有锁。
- GC停顿/网络延迟:客户端在GC期间,锁过期了,但客户端不知道,依然去操作资源。
我的建议:
- 如果你只是用单机Redis,或者主从架构(能容忍主挂了丢锁的风险),直接用普通的
SET NX或者Redisson就够了,别搞那么复杂。
- 如果你是在金融级场景,要求极高的强一致性,且网络环境复杂,RedLock也不是银弹。这时候可能要考虑基于Raft协议的系统(比如etcd、TiKV),虽然性能比Redis差,但一致性更强。
- 对于大多数互联网业务,RedLock有点“杀鸡用牛刀”,而且维护成本高。除非你真的需要多节点容错,否则单机+持久化+AOF通常够用了。
📖 学习建议:别盲目跟风用RedLock。在Redis 7.x环境下,如果你用的是Redis Cluster,其实普通的分布式锁(基于单个Master节点)配合合理的重试机制,在99%的场景下都是稳的。RedLock带来的性能损耗和复杂度,往往得不偿失。
---
6. 2024趋势:Redis Functions与云原生Serverless锁优化
技术圈更新太快了,作为老鸟,咱们不能只盯着SETNX看。2024年到2026年,Redis分布式锁也在进化。特别是Redis 7.0+引入的Functions特性,以及云原生环境下的新玩法,咱们得跟上节奏。
6.1 Redis Functions:Lua脚本的进化版
以前我们写锁逻辑,要么在客户端写Lua脚本,要么用Redisson。但Lua脚本有个痛点:它是“无状态”的,每次执行都要把脚本发给Redis,或者依赖EVALSHA缓存。而且脚本多了不好管理,版本控制也是个麻烦事。
Redis 7.0引入了Functions(虽然7.2.5已经很稳定了),打个比方,把Lua脚本作为一等公民存储在Redis服务端。你可以像调用普通命令一样调用Function。
用Function封装锁逻辑的好处:
- 性能更好:不需要每次传输脚本,服务端直接加载。
- 管理方便:可以用
FUNCTION LIST查看所有函数,用FUNCTION DELETE删除。
- 可维护性:逻辑集中在服务端,客户端只需要调用函数名。
下面是一个利用Redis Functions封装加锁逻辑的示例(假设在Redis服务端已经加载了这个函数):
-- 这是存储在Redis里的Function,名字叫 "acquireLock"
-- 加载命令:redis-cli FUNCTION LOAD "#!lua name=acquireLock \n redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])"
-- 实际开发中,Function的加载通常由运维或初始化脚本完成
客户端调用(Java示例):
import redis.clients.jedis.Jedis;
import redis.clients.jedis.args.FunctionFlags;
public class RedisFunctionLock {
private Jedis jedis;
public RedisFunctionLock() {
this.jedis = new Jedis("127.0.0.1", 6379);
}
public boolean tryLockWithFunction(String lockKey, String value, int expireMs) {
try {
// 调用名为 acquireLock 的函数
// 注意:这里演示的是调用逻辑,实际Function需要先通过 redis-cli 加载到Redis 7.2.5中
Object result = jedis.fcall("acquireLock", 1, lockKey, value, String.valueOf(expireMs));
if ("OK".equals(result)) {
System.out.println("通过Function加锁成功!");
return true;
}
return false;
} catch (Exception e) {
// 如果Function不存在,会报错,这里需要处理降级逻辑
System.err.println("Function调用失败,可能需要加载脚本: " + e.getMessage());
return false;
}
}
}
虽然现在大部分公司还在用Lua脚本,但Redis Functions绝对是未来的趋势,特别是当你需要把锁逻辑标准化、服务化的时候。
6.2 Serverless与云原生锁优化
现在搞云原生,Serverless(无服务器架构)越来越火。但这对Redis锁提出了新挑战:
- 连接开销:Serverless函数是冷启动的,每次启动都去建个Redis连接,用完就扔,这对Redis连接池压力巨大。
- 生命周期短:Serverless函数执行时间通常很短(毫秒级),传统的长连接、看门狗机制可能不太适用。
2024年的优化方向:
- 短生命周期锁:针对Serverless场景,设计更轻量的锁,甚至利用Redis的内存淘汰策略配合Key的TTL来做极简锁,减少客户端逻辑。
- 连接复用与代理:在Serverless环境和Redis之间加一层代理(比如CloudFlare Workers或者云厂商提供的Redis代理),减少连接建立的开销。
- 与Raft/TiKV融合:虽然Redis很快,但在强一致场景下,社区也在讨论如何把Redis的简单API和Raft协议的强一致性结合起来。比如,有些公司开始尝试用Dragonfly(一个兼容Redis协议的现代化存储)或者TiKV来替代Redis做锁,虽然性能略有下降,但一致性更强,省去了RedLock的复杂度。
💡 经验总结:如果你在做Serverless开发,千万别在代码里频繁创建和销毁Jedis/Redisson客户端。尽量使用单例模式或者外部连接池(如果平台支持),或者考虑使用云服务商提供的Serverless Redis实例,它们通常针对这种场景做了优化。
其实,技术选型没有最好的,只有最合适的。Redis 7.2.5依然是目前最主流的选择,但咱们得知道,除了SETNX,还有Functions,还有云原生的新玩法,这样出去面试或者做架构设计,才能显得你不仅懂原理,还懂趋势。