
分布式锁是分布式系统中解决资源竞争的核心机制,而基于 Redis 实现的分布式锁因其高性能和易实现性被广泛采用。本文将从底层原理出发,结合具体源码实现,深入剖析 Redis 分布式锁的核心逻辑,包括加锁、解锁、续期等关键环节,并探讨其可能存在的问题及解决方案。
Redis 分布式锁的实现基于其单线程特性和原子性命令,核心原理可以概括为:
Redis 官方推荐的分布式锁实现方案(Redlock 算法)虽然更健壮,但在大多数场景下,基于单 Redis 节点的实现已能满足需求,且实现简单、性能优异。
Redis 分布式锁的实现依赖几个关键命令,理解这些命令的行为是掌握分布式锁原理的基础:
# 加锁命令SET lock:resource uuid NX PX 30000该命令的返回值:
解锁需要先判断锁的持有者是否为当前客户端,再删除锁,这两个操作需要原子性执行:
-- 解锁脚本if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1])else return 0end以 Spring Data Redis 为例,我们来分析分布式锁的完整实现:
public class RedisDistributedLock implements AutoCloseable { private final RedisTemplate<String, Object> redisTemplate; private final String lockKey; // 锁的键名 private final String lockValue; // 锁的唯一标识 private final long expireTime; // 过期时间 private final TimeUnit timeUnit; // 时间单位 private boolean isLocked = false; // 是否持有锁 private ScheduledExecutorService scheduler; // 续期定时器 // 构造函数 public RedisDistributedLock(RedisTemplate<String, Object> redisTemplate, String lockKey, long expireTime, TimeUnit timeUnit) { this.redisTemplate = redisTemplate; this.lockKey = lockKey; this.expireTime = expireTime; this.timeUnit = timeUnit; // 生成唯一标识(UUID+线程ID,增强唯一性) this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId(); }锁标识设计:采用UUID+线程ID的组合,既保证不同客户端的唯一性,也避免同一客户端内不同线程的干扰。
/** * 尝试获取锁 * @param waitTime 最大等待时间 * @return 是否获取成功 */public boolean tryLock(long waitTime) throws InterruptedException { long start = System.currentTimeMillis(); long waitMillis = timeUnit.toMillis(waitTime); // 循环尝试获取锁,直到超时 while (true) { // 执行加锁命令 Boolean success = redisTemplate.opsForValue().setIfAbsent( lockKey, lockValue, expireTime, timeUnit ); if (Boolean.TRUE.equals(success)) { isLocked = true; // 启动续期任务 startRenewal(); return true; } // 检查是否超时 if (System.currentTimeMillis() - start > waitMillis) { return false; } // 短暂休眠后重试,减轻Redis压力 Thread.sleep(100); }}/** * 立即获取锁,不等待 */public boolean tryLock() { try { return tryLock(0); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; }}加锁逻辑要点:
/** * 启动锁续期任务 */private void startRenewal() { scheduler = Executors.newSingleThreadScheduledExecutor( r -> { Thread thread = new Thread(r, "lock-renewer-" + lockKey); thread.setDaemon(true); // 守护线程,避免阻塞JVM退出 return thread; } ); // 每过期时间的1/3执行一次续期 long period = timeUnit.toMillis(expireTime) / 3; scheduler.scheduleAtFixedRate(() -> { if (isLocked) { // 续期命令:延长锁的过期时间 Boolean success = redisTemplate.expire(lockKey, expireTime, timeUnit); if (Boolean.FALSE.equals(success)) { // 续期失败,可能锁已过期,停止续期 isLocked = false; scheduler.shutdown(); } } else { scheduler.shutdown(); } }, period, period, TimeUnit.MILLISECONDS);}续期机制:
/** * 释放锁 */public void unlock() { if (!isLocked) { return; } // 执行解锁Lua脚本 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) " + "else return 0 end"; Long result = (Long) redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), lockValue ); // 停止续期任务 if (scheduler != null) { scheduler.shutdown(); } isLocked = false;}/** * 实现AutoCloseable接口,支持try-with-resources语法 */@Overridepublic void close() { unlock();}解锁逻辑要点:
// 服务层使用分布式锁示例@Servicepublic class InventoryService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private InventoryMapper inventoryMapper; /** * 扣减库存 */ public boolean deductInventory(Long productId, int quantity) { // 创建分布式锁,锁的键名为"lock:inventory:商品ID",过期时间30秒 String lockKey = "lock:inventory:" + productId; try (RedisDistributedLock lock = new RedisDistributedLock( redisTemplate, lockKey, 30, TimeUnit.SECONDS)) { // 尝试获取锁,最多等待5秒 if (!lock.tryLock(5000)) { throw new RuntimeException("系统繁忙,请稍后重试"); } // 执行业务逻辑 Inventory inventory = inventoryMapper.selectByProductId(productId); if (inventory == null || inventory.getStock() < quantity) { return false; } inventory.setStock(inventory.getStock() - quantity); inventoryMapper.updateById(inventory); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } }}使用注意事项:
问题:如果任务执行时间超过锁的过期时间,锁会被自动释放,可能导致多个客户端同时执行临界区代码。
解决方案:
问题:如果 Redis 单点故障,可能导致锁服务不可用,或在主从切换时出现锁丢失。
解决方案:
问题:大量客户端同时竞争同一把锁时,会产生 "惊群效应",导致 Redis 压力增大。
解决方案:
问题:不同客户端的系统时间不一致,可能导致锁的过期判断出现偏差。
解决方案:
除了自研实现,业界还有多个成熟的开源分布式锁框架可供选择:
框架 | 特点 | 适用场景 |
|---|---|---|
Redisson | 功能全面,支持自动续期、公平锁、读写锁等 | 大多数分布式场景,尤其是需要复杂锁功能的场景 |
Curator | 基于 ZooKeeper 实现,一致性强,但性能略低 | 对一致性要求极高,可接受稍低性能的场景 |
Sentinel | 阿里开源,结合了流量控制和分布式锁 | 阿里生态系统,或已使用 Sentinel 的项目 |
以 Redisson 为例,其分布式锁的使用方式更为简洁:
// Redisson分布式锁使用示例@Servicepublic class OrderService { @Autowired private RedissonClient redissonClient; public void createOrder(Long userId) { RLock lock = redissonClient.getLock("lock:order:" + userId); try { // 尝试获取锁,等待100秒,10秒后自动释放 boolean locked = lock.tryLock(100, 10, TimeUnit.SECONDS); if (locked) { // 执行订单创建逻辑 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 确保释放锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }}Redisson 的优势在于:
Redis 分布式锁的实现看似简单,实则涉及诸多细节:从原子命令的使用到续期机制的设计,从异常处理到性能优化,每一个环节都需要仔细考量。
通过本文的源码解析,我们可以看到一个健壮的分布式锁实现需要具备:
在实际项目中,建议优先使用成熟的开源框架(如 Redisson)而非重复造轮子,但理解其底层实现原理,有助于我们更好地使用和排查问题。
分布式锁作为分布式系统中的基础组件,其稳定性直接影响整个系统的可靠性,因此在设计和使用时,必须结合业务场景综合考虑各种边界情况,才能构建出真正可靠的分布式锁服务。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。