分布式锁是分布式系统中解决资源竞争问题的重要机制。Redis凭借其高性能和原子性操作,成为实现分布式锁的热门选择。本文将详细介绍如何使用Java和Redis实现分布式锁,并重点讲解如何通过Lua脚本保证锁操作的原子性。
一、分布式锁的基本要求
一个可靠的分布式锁应满足以下条件:
-
互斥性:同一时刻只有一个客户端能持有锁
-
避免死锁:即使客户端崩溃,锁也能自动释放
-
容错性:只要大部分Redis节点正常运行,客户端就能获取和释放锁
-
释放锁的正确性:只能由锁的持有者释放锁
二、不使用Lua脚本的基础实现
1. 基础分布式锁实现(Java + StringRedisTemplate)
java
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. 基础实现的使用示例
java
@RestController
public class OrderController {
@Autowired
private 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脚本实现的分布式锁
java
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脚本语法说明
Lua
-- KEYS[1] 表示第一个键参数
-- ARGV[1] 表示第一个非键参数
-- redis.call() 用于执行Redis命令
-- 脚本逻辑:
if redis.call('get', KEYS[1]) == ARGV[1] then -- 如果锁的值等于传入的值
return redis.call('del', KEYS[1]) -- 则删除这个键
else
return 0 -- 否则返回0表示失败
end
Lua脚本在Redis中的优势
-
原子性:整个脚本作为一个整体执行,执行期间不会被其他命令打断
-
减少网络开销:多个操作合并为一个脚本,减少客户端与Redis的交互次数
-
灵活性:可以编写复杂的逻辑来处理各种场景
3. Lua脚本实现的使用示例
java
@RestController
public class PaymentController {
@Autowired
private 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. 带重试的获取锁脚本
java
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. 锁续期脚本
java
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等成熟框架,它们提供了更完善的分布式锁实现