|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
在分布式系统中,数据一致性和并发控制是至关重要的挑战。随着系统规模的扩大和微服务架构的普及,多个进程或线程同时访问共享资源的情况变得日益普遍。为了确保数据的一致性和防止并发冲突,分布式锁应运而生。Redis作为一种高性能的内存数据库,因其快速、简单的特性,成为了实现分布式锁的热门选择。然而,Redis分布式锁的正确实现和使用并非易事,特别是锁的释放环节,常常隐藏着各种陷阱。本文将深入剖析Redis分布式锁的释放问题,从锁机制原理到常见陷阱,再到解决方案,帮助读者全面理解并正确应对分布式系统中的锁管理挑战。
Redis分布式锁的基本原理
什么是分布式锁
分布式锁是一种在分布式环境下,控制多个进程或线程对共享资源访问的同步机制。与单机环境下的锁不同,分布式锁需要跨越网络和进程边界,确保在分布式系统中同一时间只有一个客户端能够持有锁并访问共享资源。
Redis分布式锁的基本实现
Redis分布式锁的基本实现通常利用Redis的SETNX命令(SET if Not eXists)。这个命令在指定的键不存在时,为键设置值,并返回1;如果键已经存在,则不做任何操作,返回0。这种特性使得SETNX非常适合用于实现锁机制。
基本的Redis分布式锁获取过程如下:
- SETNX lock_key unique_value
复制代码
其中,lock_key是锁的名称,unique_value是一个唯一标识符,通常可以使用UUID或客户端标识符,用于区分不同的锁持有者。
为了防止锁被无限期持有(例如,客户端获取锁后崩溃),通常会为锁设置一个过期时间:
- EXPIRE lock_key 30 # 设置锁30秒后自动过期
复制代码
在Redis 2.6.12及以上版本,可以使用SET命令的扩展选项,将获取锁和设置过期时间合并为一个原子操作:
- SET lock_key unique_value NX PX 30000 # NX表示不存在时才设置,PX表示过期时间单位为毫秒
复制代码
锁的释放机制
锁的释放通常通过DEL命令完成:
然而,简单的DEL命令可能导致问题:如果一个客户端的锁已经过期,而另一个客户端已经获取了相同的锁,那么第一个客户端执行DEL命令可能会错误地释放第二个客户端的锁。
为了避免这种情况,正确的锁释放应该是一个两步操作:
1. 检查锁的值是否与当前客户端的唯一标识符匹配
2. 如果匹配,则删除锁
这个过程需要是原子的,以避免竞态条件。在Redis中,可以使用Lua脚本来实现原子操作:
- if redis.call("get", KEYS[1]) == ARGV[1] then
- return redis.call("del", KEYS[1])
- else
- return 0
- end
复制代码
Redis分布式锁的释放机制详解
锁释放的正确流程
Redis分布式锁的正确释放流程应该包含以下步骤:
1. 客户端生成唯一标识符(如UUID)
2. 使用SET命令获取锁,并设置过期时间
3. 执行需要同步的代码
4. 释放锁前,先验证锁的值是否与自己的唯一标识符匹配
5. 如果匹配,则释放锁;如果不匹配,说明锁已经过期或被其他客户端获取,不应该释放
为什么需要验证锁的所有者
验证锁的所有者是防止误删其他客户端持有的锁的关键。考虑以下场景:
1. 客户端A获取锁,设置过期时间为30秒
2. 客户端A执行的操作耗时超过30秒
3. 锁自动过期
4. 客户端B获取了同一个锁
5. 客户端A完成操作,尝试释放锁
如果客户端A不验证锁的所有者就直接释放锁,它会错误地释放客户端B持有的锁,导致数据不一致或其他并发问题。
锁的自动过期与手动释放
锁的自动过期是一种保护机制,防止客户端崩溃后锁永远不被释放。然而,自动过期也带来了新的挑战:
1. 过期时间设置:过期时间太短可能导致操作未完成锁就过期;过期时间太长可能导致客户端崩溃后锁长时间不被释放。
2. 锁续期:对于长时间运行的操作,可能需要在锁过期前主动续期。
Redis分布式锁的原子性要求
Redis分布式锁的获取和释放都需要保证原子性:
• 获取锁的原子性:使用SET命令的NX和EX/PX选项,确保获取锁和设置过期时间是原子操作。
• 释放锁的原子性:使用Lua脚本确保检查锁所有者和释放锁是原子操作,避免竞态条件。
常见的锁释放陷阱
陷阱一:误删其他客户端的锁
这是最常见的陷阱之一。当客户端不验证锁的所有者就直接释放锁时,可能会误删其他客户端的锁。
问题示例:
- // 错误的锁释放示例
- public void releaseLock(String lockKey) {
- jedis.del(lockKey); // 直接删除锁,不验证所有者
- }
复制代码
解决方案:
使用Lua脚本确保只有锁的持有者才能释放锁:
- // 正确的锁释放示例
- public boolean releaseLock(String lockKey, String uniqueValue) {
- String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- "return redis.call('del', KEYS[1]) " +
- "else " +
- "return 0 " +
- "end";
- Long result = (Long) jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(uniqueValue));
- return result == 1;
- }
复制代码
陷阱二:锁过期导致业务未完成
锁的过期时间设置不当可能导致业务操作未完成锁就过期,从而引发并发问题。
问题示例:
- // 锁过期时间设置过短
- public void doBusiness() {
- String lockKey = "resource_lock";
- String uniqueValue = UUID.randomUUID().toString();
-
- // 获取锁,设置过期时间为1秒
- boolean locked = jedis.set(lockKey, uniqueValue, "NX", "PX", 1000) != null;
-
- if (locked) {
- try {
- // 模拟耗时操作,超过1秒
- Thread.sleep(1500);
- // 业务逻辑
- businessLogic();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- releaseLock(lockKey, uniqueValue);
- }
- }
- }
复制代码
解决方案:
1. 合理设置过期时间:根据业务操作的预估耗时设置合适的过期时间,并留有一定的缓冲。
2. 锁续期机制:实现锁的自动续期,确保在业务操作完成前锁不会过期。
- // 锁续期示例
- public void doBusinessWithLockRenewal() {
- String lockKey = "resource_lock";
- String uniqueValue = UUID.randomUUID().toString();
-
- // 获取锁,设置过期时间为10秒
- boolean locked = jedis.set(lockKey, uniqueValue, "NX", "PX", 10000) != null;
-
- if (locked) {
- // 启动一个后台线程定期续期
- ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
- ScheduledFuture<?> renewalFuture = scheduler.scheduleAtFixedRate(() -> {
- // 续期锁,将过期时间重新设置为10秒
- jedis.pexpire(lockKey, 10000);
- }, 3, 3, TimeUnit.SECONDS); // 每3秒续期一次
-
- try {
- // 业务逻辑
- businessLogic();
- } finally {
- // 停止续期
- renewalFuture.cancel(true);
- scheduler.shutdown();
- // 释放锁
- releaseLock(lockKey, uniqueValue);
- }
- }
- }
复制代码
陷阱三:Redis单点故障导致锁信息丢失
使用单个Redis实例作为分布式锁服务存在单点故障风险。如果Redis实例宕机,所有锁信息将丢失,可能导致多个客户端同时获取到同一把锁。
解决方案:
使用Redis集群或Redis Sentinel实现高可用,或者采用Redlock算法(Redis分布式锁算法)来增强可靠性。
Redlock算法的基本思想是:
1. 获取当前时间戳
2. 按顺序依次向N个Redis实例请求获取锁
3. 计算获取锁所花费的时间
4. 如果客户端能够在大多数Redis实例上获取锁,并且获取锁的总时间小于锁的有效时间,则认为锁获取成功
5. 如果获取锁失败,则尝试释放所有已获取的锁
- // Redlock算法示例实现
- public class RedLock {
- private final List<Jedis> jedisInstances;
-
- public RedLock(List<Jedis> jedisInstances) {
- this.jedisInstances = jedisInstances;
- }
-
- public boolean lock(String lockKey, String uniqueValue, long expireTime) {
- int N = jedisInstances.size();
- long startTime = System.currentTimeMillis();
- int acquiredCount = 0;
-
- // 尝试在所有Redis实例上获取锁
- for (Jedis jedis : jedisInstances) {
- String result = jedis.set(lockKey, uniqueValue, "NX", "PX", expireTime);
- if ("OK".equals(result)) {
- acquiredCount++;
- }
- }
-
- // 计算获取锁所花费的时间
- long elapsedTime = System.currentTimeMillis() - startTime;
-
- // 如果在大多数实例上获取锁成功,并且获取锁的时间小于锁的有效时间
- if (acquiredCount > N/2 && elapsedTime < expireTime) {
- return true;
- } else {
- // 获取锁失败,释放所有已获取的锁
- unlock(lockKey, uniqueValue);
- return false;
- }
- }
-
- public void unlock(String lockKey, String uniqueValue) {
- String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- "return redis.call('del', KEYS[1]) " +
- "else " +
- "return 0 " +
- "end";
-
- for (Jedis jedis : jedisInstances) {
- try {
- jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(uniqueValue));
- } catch (Exception e) {
- // 忽略异常,继续尝试释放其他实例上的锁
- }
- }
- }
- }
复制代码
陷阱四:网络分区导致的脑裂问题
在分布式系统中,网络分区可能导致Redis集群出现脑裂(Split-Brain)问题,即多个分区都认为自己是主节点,从而可能导致多个客户端同时获取到同一把锁。
解决方案:
1. 使用Redis集群的自动故障转移机制,确保在网络分区后只有一个主节点。
2. 使用Redlock算法,即使出现网络分区,也能保证锁的安全性。
3. 实现 fencing token 机制,为每个锁请求分配一个递增的令牌,服务端在处理请求时验证令牌的有效性和顺序。
陷阱五:GC暂停导致的锁过期
在Java应用中,长时间的垃圾回收(GC)暂停可能导致线程暂停执行,从而使得锁过期,而客户端并不知情。
解决方案:
1. 优化GC:调整JVM参数,减少长时间GC暂停的可能性。
2. 锁续期:实现锁的自动续期机制,确保在GC暂停期间锁不会过期。
3. 使用更安全的锁实现:如Zookeeper分布式锁,它通过临时节点和会话机制提供了更可靠的锁管理。
- // 使用Zookeeper实现分布式锁示例
- public class ZookeeperDistributedLock {
- private final ZooKeeper zk;
- private final String lockPath;
- private String currentLockPath;
-
- public ZookeeperDistributedLock(ZooKeeper zk, String lockPath) {
- this.zk = zk;
- this.lockPath = lockPath;
- }
-
- public boolean lock(long timeout, TimeUnit unit) throws InterruptedException, KeeperException {
- long startTime = System.currentTimeMillis();
- long timeoutMillis = unit.toMillis(timeout);
-
- // 创建临时顺序节点
- currentLockPath = zk.create(lockPath + "/lock-", new byte[0],
- ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
-
- // 获取所有子节点并排序
- List<String> children = zk.getChildren(lockPath, false);
- Collections.sort(children);
-
- // 检查当前节点是否是最小的节点
- String currentNode = currentLockPath.substring(lockPath.length() + 1);
- int currentIndex = children.indexOf(currentNode);
-
- if (currentIndex == 0) {
- // 当前节点是最小的,获取锁成功
- return true;
- } else {
- // 监听前一个节点
- String previousNodePath = lockPath + "/" + children.get(currentIndex - 1);
- final CountDownLatch latch = new CountDownLatch(1);
- Stat stat = zk.exists(previousNodePath, new Watcher() {
- @Override
- public void process(WatchedEvent event) {
- if (event.getType() == Event.EventType.NodeDeleted) {
- latch.countDown();
- }
- }
- });
-
- if (stat == null) {
- // 前一个节点已经不存在,重新尝试获取锁
- return lock(timeoutMillis - (System.currentTimeMillis() - startTime), TimeUnit.MILLISECONDS);
- }
-
- // 等待前一个节点被删除或超时
- latch.await(timeoutMillis - (System.currentTimeMillis() - startTime), TimeUnit.MILLISECONDS);
-
- // 重新检查当前节点是否是最小的节点
- children = zk.getChildren(lockPath, false);
- Collections.sort(children);
- currentIndex = children.indexOf(currentNode);
-
- return currentIndex == 0;
- }
- }
-
- public void unlock() throws KeeperException, InterruptedException {
- if (currentLockPath != null) {
- zk.delete(currentLockPath, -1);
- currentLockPath = null;
- }
- }
- }
复制代码
解决方案与最佳实践
1. 使用Redisson实现可靠的分布式锁
Redisson是一个Redis的Java客户端,提供了许多分布式数据结构和服务,包括分布式锁。Redisson实现的分布式锁解决了上述许多问题,如锁续期、锁释放的安全性等。
- // 使用Redisson实现分布式锁
- public class RedissonLockExample {
- public void doBusiness() {
- Config config = new Config();
- config.useSingleServer().setAddress("redis://127.0.0.1:6379");
- RedissonClient redisson = Redisson.create(config);
-
- RLock lock = redisson.getLock("myLock");
-
- try {
- // 获取锁,设置锁的自动续期时间为30秒
- lock.lock(30, TimeUnit.SECONDS);
-
- // 执行业务逻辑
- businessLogic();
-
- } finally {
- // 释放锁
- lock.unlock();
- }
- }
-
- private void businessLogic() {
- // 业务逻辑实现
- }
- }
复制代码
Redisson的分布式锁具有以下特性:
1. 自动续期:Redisson会自动为锁续期,防止业务未完成锁就过期。
2. 可重入:同一个线程可以多次获取同一把锁。
3. 锁等待:支持在获取锁失败时等待一段时间。
4. 锁释放的安全性:确保只有锁的持有者才能释放锁。
2. 实现锁续期机制
如果不使用Redisson,也可以自己实现锁续期机制:
- public class LockRenewalExample {
- private final Jedis jedis;
- private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
-
- public LockRenewalExample(Jedis jedis) {
- this.jedis = jedis;
- }
-
- public void doBusinessWithLockRenewal() {
- String lockKey = "resource_lock";
- String uniqueValue = UUID.randomUUID().toString();
- long expireTime = 10000; // 10秒
-
- // 获取锁
- boolean locked = jedis.set(lockKey, uniqueValue, "NX", "PX", expireTime) != null;
-
- if (locked) {
- ScheduledFuture<?> renewalFuture = null;
- try {
- // 启动锁续期任务,每3秒续期一次
- renewalFuture = scheduler.scheduleAtFixedRate(() -> {
- jedis.pexpire(lockKey, expireTime);
- }, expireTime / 3, expireTime / 3, TimeUnit.MILLISECONDS);
-
- // 执行业务逻辑
- businessLogic();
-
- } finally {
- // 停止锁续期
- if (renewalFuture != null) {
- renewalFuture.cancel(true);
- }
-
- // 释放锁
- releaseLock(lockKey, uniqueValue);
- }
- }
- }
-
- private boolean releaseLock(String lockKey, String uniqueValue) {
- String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- "return redis.call('del', KEYS[1]) " +
- "else " +
- "return 0 " +
- "end";
- Long result = (Long) jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(uniqueValue));
- return result == 1;
- }
-
- private void businessLogic() {
- // 业务逻辑实现
- }
- }
复制代码
3. 使用Redlock算法提高可靠性
对于对锁安全性要求极高的场景,可以使用Redlock算法:
- public class RedLockExample {
- private final List<Jedis> jedisInstances;
-
- public RedLockExample(List<Jedis> jedisInstances) {
- this.jedisInstances = jedisInstances;
- }
-
- public void doBusinessWithRedLock() {
- String lockKey = "resource_lock";
- String uniqueValue = UUID.randomUUID().toString();
- long expireTime = 10000; // 10秒
-
- RedLock redLock = new RedLock(jedisInstances);
- boolean locked = redLock.lock(lockKey, uniqueValue, expireTime);
-
- if (locked) {
- try {
- // 执行业务逻辑
- businessLogic();
- } finally {
- // 释放锁
- redLock.unlock(lockKey, uniqueValue);
- }
- } else {
- // 获取锁失败的处理
- System.out.println("Failed to acquire lock");
- }
- }
-
- private void businessLogic() {
- // 业务逻辑实现
- }
- }
复制代码
4. 实现锁的容错机制
在分布式系统中,网络故障和节点故障是常态,因此需要实现锁的容错机制:
- public class FaultTolerantLockExample {
- private final Jedis jedis;
- private final int maxRetryTimes;
- private final long retryInterval;
-
- public FaultTolerantLockExample(Jedis jedis, int maxRetryTimes, long retryInterval) {
- this.jedis = jedis;
- this.maxRetryTimes = maxRetryTimes;
- this.retryInterval = retryInterval;
- }
-
- public void doBusinessWithFaultTolerantLock() {
- String lockKey = "resource_lock";
- String uniqueValue = UUID.randomUUID().toString();
- long expireTime = 10000; // 10秒
-
- int retryTimes = 0;
- boolean locked = false;
-
- // 重试机制
- while (retryTimes < maxRetryTimes && !locked) {
- try {
- locked = jedis.set(lockKey, uniqueValue, "NX", "PX", expireTime) != null;
-
- if (locked) {
- try {
- // 执行业务逻辑
- businessLogic();
- } finally {
- // 释放锁
- releaseLock(lockKey, uniqueValue);
- }
- } else {
- retryTimes++;
- if (retryTimes < maxRetryTimes) {
- Thread.sleep(retryInterval);
- }
- }
- } catch (Exception e) {
- retryTimes++;
- if (retryTimes < maxRetryTimes) {
- try {
- Thread.sleep(retryInterval);
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- break;
- }
- }
- }
- }
-
- if (!locked) {
- // 获取锁失败的处理
- System.out.println("Failed to acquire lock after " + maxRetryTimes + " retries");
- }
- }
-
- private boolean releaseLock(String lockKey, String uniqueValue) {
- try {
- String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- "return redis.call('del', KEYS[1]) " +
- "else " +
- "return 0 " +
- "end";
- Long result = (Long) jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(uniqueValue));
- return result == 1;
- } catch (Exception e) {
- // 释放锁失败的处理
- System.err.println("Failed to release lock: " + e.getMessage());
- return false;
- }
- }
-
- private void businessLogic() {
- // 业务逻辑实现
- }
- }
复制代码
5. 监控和告警机制
实现分布式锁的监控和告警机制,及时发现和处理锁相关的问题:
- public class LockMonitorExample {
- private final Jedis jedis;
- private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
- private final Map<String, Long> lockAcquisitionTimes = new ConcurrentHashMap<>();
- private final long lockHoldTimeThreshold;
-
- public LockMonitorExample(Jedis jedis, long lockHoldTimeThreshold) {
- this.jedis = jedis;
- this.lockHoldTimeThreshold = lockHoldTimeThreshold;
- }
-
- public void startMonitoring() {
- // 定期检查锁的持有时间
- scheduler.scheduleAtFixedRate(() -> {
- long currentTime = System.currentTimeMillis();
- lockAcquisitionTimes.forEach((lockKey, acquisitionTime) -> {
- long holdTime = currentTime - acquisitionTime;
- if (holdTime > lockHoldTimeThreshold) {
- // 锁持有时间超过阈值,发出告警
- System.out.println("Warning: Lock " + lockKey + " has been held for " + holdTime + "ms");
- }
- });
- }, 1, 1, TimeUnit.MINUTES);
-
- // 定期检查Redis中的锁
- scheduler.scheduleAtFixedRate(() -> {
- try {
- Set<String> keys = jedis.keys("*lock*");
- for (String key : keys) {
- String value = jedis.get(key);
- if (value != null && !lockAcquisitionTimes.containsKey(key)) {
- // 发现未记录的锁,可能是由于应用重启导致的
- System.out.println("Warning: Found untracked lock: " + key);
- }
- }
- } catch (Exception e) {
- System.err.println("Failed to check locks in Redis: " + e.getMessage());
- }
- }, 5, 5, TimeUnit.MINUTES);
- }
-
- public void recordLockAcquisition(String lockKey) {
- lockAcquisitionTimes.put(lockKey, System.currentTimeMillis());
- }
-
- public void recordLockRelease(String lockKey) {
- lockAcquisitionTimes.remove(lockKey);
- }
-
- public void stopMonitoring() {
- scheduler.shutdown();
- try {
- if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
- scheduler.shutdownNow();
- }
- } catch (InterruptedException e) {
- scheduler.shutdownNow();
- Thread.currentThread().interrupt();
- }
- }
- }
复制代码
实际应用场景与案例分析
场景一:电商秒杀系统
在电商秒杀系统中,多个用户同时抢购有限数量的商品,需要使用分布式锁来控制库存的扣减,防止超卖。
- public class SecKillExample {
- private final Jedis jedis;
- private final String productKey = "product:1001:stock";
-
- public SecKillExample(Jedis jedis) {
- this.jedis = jedis;
- }
-
- public boolean secKill(String userId) {
- String lockKey = "secKill:lock:product:1001";
- String uniqueValue = UUID.randomUUID().toString();
- long expireTime = 10000; // 10秒
-
- // 获取锁
- boolean locked = jedis.set(lockKey, uniqueValue, "NX", "PX", expireTime) != null;
-
- if (!locked) {
- System.out.println("User " + userId + " failed to acquire lock");
- return false;
- }
-
- try {
- // 检查库存
- String stockStr = jedis.get(productKey);
- int stock = stockStr == null ? 0 : Integer.parseInt(stockStr);
-
- if (stock <= 0) {
- System.out.println("User " + userId + " failed: out of stock");
- return false;
- }
-
- // 扣减库存
- jedis.decr(productKey);
-
- // 创建订单
- createOrder(userId);
-
- System.out.println("User " + userId + " successfully purchased");
- return true;
-
- } finally {
- // 释放锁
- releaseLock(lockKey, uniqueValue);
- }
- }
-
- private boolean releaseLock(String lockKey, String uniqueValue) {
- String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- "return redis.call('del', KEYS[1]) " +
- "else " +
- "return 0 " +
- "end";
- Long result = (Long) jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(uniqueValue));
- return result == 1;
- }
-
- private void createOrder(String userId) {
- // 创建订单的逻辑
- System.out.println("Creating order for user " + userId);
- }
- }
复制代码
场景二:定时任务分布式执行
在分布式系统中,定时任务需要确保只有一个节点执行,避免重复执行。
- public class DistributedSchedulerExample {
- private final Jedis jedis;
- private final String taskId;
- private final long lockExpireTime;
-
- public DistributedSchedulerExample(Jedis jedis, String taskId, long lockExpireTime) {
- this.jedis = jedis;
- this.taskId = taskId;
- this.lockExpireTime = lockExpireTime;
- }
-
- public void executeTask(Runnable task) {
- String lockKey = "scheduler:lock:" + taskId;
- String uniqueValue = UUID.randomUUID().toString();
-
- // 获取锁
- boolean locked = jedis.set(lockKey, uniqueValue, "NX", "PX", lockExpireTime) != null;
-
- if (!locked) {
- System.out.println("Task " + taskId + " is already running on another node");
- return;
- }
-
- try {
- System.out.println("Starting task " + taskId);
-
- // 执行任务
- task.run();
-
- System.out.println("Completed task " + taskId);
-
- } finally {
- // 释放锁
- releaseLock(lockKey, uniqueValue);
- }
- }
-
- private boolean releaseLock(String lockKey, String uniqueValue) {
- String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- "return redis.call('del', KEYS[1]) " +
- "else " +
- "return 0 " +
- "end";
- Long result = (Long) jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(uniqueValue));
- return result == 1;
- }
- }
复制代码
场景三:分布式事务中的数据一致性
在分布式事务中,需要使用分布式锁来确保多个操作之间的数据一致性。
- public class DistributedTransactionExample {
- private final Jedis jedis;
- private final DataSource dataSource1;
- private final DataSource dataSource2;
-
- public DistributedTransactionExample(Jedis jedis, DataSource dataSource1, DataSource dataSource2) {
- this.jedis = jedis;
- this.dataSource1 = dataSource1;
- this.dataSource2 = dataSource2;
- }
-
- public boolean executeTransaction(String accountId, BigDecimal amount) {
- String lockKey = "transaction:lock:" + accountId;
- String uniqueValue = UUID.randomUUID().toString();
- long expireTime = 30000; // 30秒
-
- // 获取锁
- boolean locked = jedis.set(lockKey, uniqueValue, "NX", "PX", expireTime) != null;
-
- if (!locked) {
- System.out.println("Failed to acquire lock for account " + accountId);
- return false;
- }
-
- Connection conn1 = null;
- Connection conn2 = null;
-
- try {
- // 获取数据库连接
- conn1 = dataSource1.getConnection();
- conn2 = dataSource2.getConnection();
-
- // 开始事务
- conn1.setAutoCommit(false);
- conn2.setAutoCommit(false);
-
- // 执行操作1
- PreparedStatement stmt1 = conn1.prepareStatement("UPDATE accounts SET balance = balance - ? WHERE id = ?");
- stmt1.setBigDecimal(1, amount);
- stmt1.setString(2, accountId);
- int result1 = stmt1.executeUpdate();
-
- // 执行操作2
- PreparedStatement stmt2 = conn2.prepareStatement("UPDATE transaction_log SET amount = amount + ? WHERE account_id = ?");
- stmt2.setBigDecimal(1, amount);
- stmt2.setString(2, accountId);
- int result2 = stmt2.executeUpdate();
-
- // 检查操作结果
- if (result1 > 0 && result2 > 0) {
- // 提交事务
- conn1.commit();
- conn2.commit();
- System.out.println("Transaction completed successfully for account " + accountId);
- return true;
- } else {
- // 回滚事务
- conn1.rollback();
- conn2.rollback();
- System.out.println("Transaction failed for account " + accountId);
- return false;
- }
-
- } catch (SQLException e) {
- // 发生异常,回滚事务
- try {
- if (conn1 != null) conn1.rollback();
- if (conn2 != null) conn2.rollback();
- } catch (SQLException ex) {
- System.err.println("Failed to rollback transaction: " + ex.getMessage());
- }
- System.err.println("Transaction error: " + e.getMessage());
- return false;
-
- } finally {
- // 释放资源
- try {
- if (conn1 != null) conn1.close();
- if (conn2 != null) conn2.close();
- } catch (SQLException e) {
- System.err.println("Failed to close connections: " + e.getMessage());
- }
-
- // 释放锁
- releaseLock(lockKey, uniqueValue);
- }
- }
-
- private boolean releaseLock(String lockKey, String uniqueValue) {
- String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
- "return redis.call('del', KEYS[1]) " +
- "else " +
- "return 0 " +
- "end";
- Long result = (Long) jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(uniqueValue));
- return result == 1;
- }
- }
复制代码
总结
Redis分布式锁是分布式系统中控制并发访问的重要工具,但正确实现和使用分布式锁并非易事。本文从Redis分布式锁的基本原理出发,详细剖析了锁释放机制,并揭示了常见的锁释放陷阱,包括误删其他客户端的锁、锁过期导致业务未完成、Redis单点故障、网络分区导致的脑裂问题以及GC暂停导致的锁过期等。
针对这些陷阱,本文提供了多种解决方案和最佳实践,包括使用Redisson实现可靠的分布式锁、实现锁续期机制、使用Redlock算法提高可靠性、实现锁的容错机制以及建立监控和告警机制等。通过实际应用场景的案例分析,展示了如何在不同场景下正确使用Redis分布式锁。
在实际应用中,选择合适的分布式锁实现方案需要综合考虑业务场景、性能要求、可靠性需求以及系统复杂度等因素。无论选择哪种方案,都需要确保锁的获取和释放是安全的,避免因锁问题导致数据不一致或系统故障。
通过深入理解Redis分布式锁的原理和陷阱,并采用适当的解决方案和最佳实践,我们可以更好地应对分布式系统中的锁管理挑战,构建更加可靠、高效的分布式系统。 |
|