Redis实现分布式锁
分布式锁是分布式系统中解决资源竞争问题的重要机制。Redis凭借其高性能和原子性操作,成为实现分布式锁的热门选择。本文将详细介绍如何使用Java和Redis实现分布式锁,并重点讲解如何通过Lua脚本保证锁操作的原子性。
一、分布式锁的基本要求
一个可靠的分布式锁应满足以下条件:
-
互斥性:同一时刻只有一个客户端能持有锁
-
避免死锁:即使客户端崩溃,锁也能自动释放
-
容错性:只要大部分Redis节点正常运行,客户端就能获取和释放锁
-
释放锁的正确性:只能由锁的持有者释放锁
二、不使用Lua脚本的基础实现
1. 基础分布式锁实现(Java + StringRedisTemplate)
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.UUID;
import java.util.concurrent.TimeUnit;@Component
public class BasicRedisLock {private final StringRedisTemplate stringRedisTemplate;public BasicRedisLock(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}/*** 获取锁* @param lockKey 锁的key* @param expireTime 过期时间(秒)* @return 锁的value(用于释放锁时验证)*/public String lock(String lockKey, long expireTime) {String value = UUID.randomUUID().toString();Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);return Boolean.TRUE.equals(success) ? value : null;}/*** 释放锁* @param lockKey 锁的key* @param value 锁的value* @return 是否释放成功*/public boolean unlock(String lockKey, String value) {// 1. 获取当前锁的值String currentValue = stringRedisTemplate.opsForValue().get(lockKey);// 2. 验证是否是自己的锁if (value.equals(currentValue)) {// 3. 删除锁return stringRedisTemplate.delete(lockKey);}return false;}
}
2. 基础实现的使用示例
@RestController
public class OrderController {@Autowiredprivate BasicRedisLock basicRedisLock;@PostMapping("/createOrder")public String createOrder() {String lockKey = "order_lock";String lockValue = null;try {// 尝试获取锁lockValue = basicRedisLock.lock(lockKey, 30);if (lockValue == null) {return "系统繁忙,请稍后再试";}// 执行业务逻辑return "订单创建成功";} finally {// 释放锁if (lockValue != null) {basicRedisLock.unlock(lockKey, lockValue);}}}
}
3. 基础实现的缺点
-
非原子性操作问题:
-
释放锁的操作分为"获取值"、"比较值"和"删除键"三步,不是原子操作
-
在比较值和删除键之间,锁可能已过期并被其他客户端获取,导致误删别人的锁
-
-
网络延迟问题:
-
客户端A获取锁并执行时间过长,锁已自动释放
-
客户端B获取了锁
-
客户端A执行完任务后,仍会尝试释放锁,可能释放客户端B的锁
-
-
性能问题:
-
每次释放锁需要至少2次Redis操作(GET+DEL)
-
三、使用Lua脚本优化实现
1. Lua脚本实现的分布式锁
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;@Component
public class LuaRedisLock {private final StringRedisTemplate stringRedisTemplate;public LuaRedisLock(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}// 获取锁(与基础实现相同)public String lock(String lockKey, long expireTime) {String value = UUID.randomUUID().toString();Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);return Boolean.TRUE.equals(success) ? value : null;}// 使用Lua脚本释放锁public boolean unlock(String lockKey, String value) {// Lua脚本String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +" return redis.call('del', KEYS[1]) " +"else " +" return 0 " +"end";// 创建Redis脚本对象DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(luaScript);redisScript.setResultType(Long.class);// 执行脚本Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(lockKey), value);return result != null && result == 1;}
}
2. Lua脚本详解
Lua脚本语法说明
-- KEYS[1] 表示第一个键参数
-- ARGV[1] 表示第一个非键参数
-- redis.call() 用于执行Redis命令-- 脚本逻辑:
if redis.call('get', KEYS[1]) == ARGV[1] then -- 如果锁的值等于传入的值return redis.call('del', KEYS[1]) -- 则删除这个键
elsereturn 0 -- 否则返回0表示失败
end
Lua脚本在Redis中的优势
-
原子性:整个脚本作为一个整体执行,执行期间不会被其他命令打断
-
减少网络开销:多个操作合并为一个脚本,减少客户端与Redis的交互次数
-
灵活性:可以编写复杂的逻辑来处理各种场景
3. Lua脚本实现的使用示例
@RestController
public class PaymentController {@Autowiredprivate LuaRedisLock luaRedisLock;@PostMapping("/pay")public String payOrder(@RequestParam String orderId) {String lockKey = "pay_lock:" + orderId;String lockValue = null;try {// 尝试获取锁,设置30秒过期lockValue = luaRedisLock.lock(lockKey, 30);if (lockValue == null) {return "支付处理中,请勿重复提交";}// 执行业务逻辑processPayment(orderId);return "支付成功";} finally {// 释放锁if (lockValue != null) {luaRedisLock.unlock(lockKey, lockValue);}}}private void processPayment(String orderId) {// 支付处理逻辑}
}
四、两种实现的对比
特性 | 基础实现 | Lua脚本实现 |
---|---|---|
原子性 | 非原子操作 | 原子操作 |
安全性 | 可能误删别人的锁 | 不会误删别人的锁 |
网络开销 | 至少2次Redis操作 | 1次Redis操作 |
实现复杂度 | 简单 | 需要了解Lua脚本 |
性能 | 较低 | 较高 |
适用场景 | 对安全性要求不高的简单场景 | 对安全性和性能有要求的场景 |
五、Lua脚本的更多用法
1. 带重试的获取锁脚本
public String lockWithRetry(String lockKey, long expireTime, long waitTime) {String value = UUID.randomUUID().toString();String luaScript = "local wait = tonumber(ARGV[3]) * 1000 " +"local endTime = redis.call('time')[1] * 1000 + wait " +"while redis.call('time')[1] * 1000 < endTime do " +" if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +" redis.call('expire', KEYS[1], ARGV[2]) " +" return ARGV[1] " +" end " +" -- 短暂休眠 " +" redis.call('echo', 'waiting...') " +"end " +"return nil";DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(luaScript);redisScript.setResultType(String.class);return stringRedisTemplate.execute(redisScript,Collections.singletonList(lockKey),value, String.valueOf(expireTime), String.valueOf(waitTime));
}
2. 锁续期脚本
public boolean renewLock(String lockKey, String value, long expireTime) {String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +" return redis.call('expire', KEYS[1], ARGV[2]) " +"else " +" return 0 " +"end";DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(luaScript);redisScript.setResultType(Long.class);Long result = stringRedisTemplate.execute(redisScript,Collections.singletonList(lockKey),value, String.valueOf(expireTime));return result != null && result == 1;
}
六、总结
-
基础实现简单但存在安全隐患,适合对一致性要求不高的场景
-
Lua脚本实现通过原子操作解决了安全问题,是生产环境推荐的做法
-
Lua脚本在Redis中执行是原子的,适合实现复杂的多步操作
-
实际应用中还可以结合Redisson等成熟框架,它们提供了更完善的分布式锁实现