IMS系统分布式锁设计完全指南

在分布式环境中,多个服务可能同时竞争同一资源,需要分布式锁来保证操作的原子性。本文将详细介绍分布式锁的实现方案、Redis与ZooKeeper实现、锁粒度设计以及常见问题解决方案。

为什么需要分布式锁

  • 多个服务实例可能同时修改同一条数据
  • 本地锁只能限制单个JVM内的并发
  • 数据库行锁在高并发下性能较差
  • 需要跨服务、跨机器的协调能力

分布式锁的核心要求

  • 互斥性 - 同一时刻只有一个客户端能获取锁
  • 可重入性 - 同一客户端可重复获取同一把锁
  • 锁超时 - 防止锁无法释放导致死锁
  • 公平性 - 按照请求顺序获取锁(可选)
  • 高可用 - 锁服务自身需要高可用

Redis 分布式锁

使用 Redis 的 SET 命令实现分布式锁:

redis-lock.java
// Redis 分布式锁实现
public class RedisDistributedLock {

    private RedisTemplate<String, String> redisTemplate;
    private String lockKey;
    private String lockValue;
    private long expireMs = 30000;  // 默认30秒
    private long waitMs = 10000;     // 默认10秒

    // 获取锁
    public boolean tryLock() {
        lockValue = UUID.randomUUID().toString();

        while (System.currentTimeMillis() < waitUntil) {
            // SET key value NX PX expireTime
            Boolean success = redisTemplate.opsForValue().setIfAbsent(
                lockKey,
                lockValue,
                Duration.ofMillis(expireMs)
            );

            if (Boolean.TRUE.equals(success)) {
                return true;
            }

            // 短暂等待后重试
            Thread.sleep(10);
        }
        return false;
    }

    // 释放锁(Lua脚本保证原子性)
    public void unlock() {
        String script =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "return redis.call('del', KEYS[1]) else return 0 end";

        redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(lockKey),
            lockValue
        );
    }
}

Redisson 使用

Redisson 提供了更完善的分布式锁实现:

redisson-lock.java
// Redisson 分布式锁
@Autowired
private RedissonClient redissonClient;

public void processOrder(Order order) {
    // 获取锁,key 粒度细化到订单ID
    RLock lock = redissonClient.getLock("order:" + order.getId());

    try {
        // 等待锁最多10秒,锁持有30秒自动释放
        lock.tryLock(10, 30, TimeUnit.SECONDS);

        // 业务逻辑
        orderService.process(order);

    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        // 释放锁
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

可重入锁实现

reentrant-lock.java
// 可重入锁实现
public class ReentrantRedisLock {

    public boolean tryLock(String key, String value, long timeout) {
        // 首次获取锁
        if (redisTemplate.opsForValue().setIfAbsent(key, value,
            Duration.ofMillis(timeout)) {

            // 记录重入次数
            redisTemplate.hashOps().increment(key + ":count", value, 1);
            return true;
        }

        // 重入:当前线程已持有锁
        String currentValue = redisTemplate.opsForValue().get(key);
        if (value.equals(currentValue)) {
            redisTemplate.hashOps().increment(key + ":count", value, 1);
            return true;
        }

        return false;
    }

    public void unlock(String key, String value) {
        Long count = redisTemplate.hashOps().increment(key + ":count", value, -1);

        // 重入计数为0时释放锁
        if (count <= 0) {
            redisTemplate.delete(key);
        }
    }
}

锁粒度设计

锁粒度直接影响并发性能:

粒度示例并发度
全局锁lock:global最低
表锁lock:order较低
行锁lock:order:1001较高
业务锁lock:order:1001:operation最高

最佳实践

  • 锁粒度尽量细化,只锁住需要保护的资源
  • 避免锁住整个方法,只锁住关键代码段
  • 考虑锁的时效性,持有时间不宜过长

ZooKeeper 分布式锁

ZooKeeper 通过临时顺序节点实现分布式锁:

zk-lock.java
// ZooKeeper 分布式锁
public class ZKDistributedLock {

    private CuratorFramework client;
    private String lockPath;

    // 获取锁
    public boolean tryLock() throws Exception {
        // 创建临时顺序节点
        String node = client.create()
            .creatingParentsIfNeeded()
            .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
            .forPath(lockPath + "/lock-");

        // 获取所有锁节点,判断是否是第一个
        List<String> children = client.getChildren().forPath(lockPath);
        Collections.sort(children);

        if (node.endsWith(children.get(0))) {
            return true;  // 获取到锁
        }

        // 监听前一个节点,阻塞等待
        String prevNode = children.get(
            Collections.binarySearch(children,
            node.substring(node.lastIndexOf("/") + 1)) - 1
        );

        // 等待前一个节点删除(锁释放)
        return waitForLock(prevNode);
    }

    private boolean waitForLock(String prevNode) throws Exception {
        CountDownLatch latch = new CountDownLatch(1);

        // 注册监听器
        client.getData().usingWatcher(new Watcher() {
            public void process(WatchedEvent event) {
                if (event.getType() == EventType.NodeDeleted) {
                    latch.countDown();
                }
            }
        }).forPath(lockPath + "/" + prevNode);

        return latch.await(30, TimeUnit.SECONDS);
    }
}

常见问题与解决方案

1. 锁超时问题

业务处理时间超过锁有效期,导致锁自动释放被其他线程获取:

  • 合理设置锁超时时间
  • 使用看门狗自动续期(Redisson支持)
  • 将长流程拆分,避免长时间持有锁

2. 脑裂问题

Redis主从切换期间可能出现锁丢失:

  • 使用RedLock算法(多节点)
  • 或使用ZooKeeper(CP模型)

3. 死锁预防

  • 始终在 finally 块释放锁
  • 设置锁超时时间
  • 使用可重入锁避免自身死锁

IMS系统应用场景

  • 库存扣减 - 防止超卖
  • 余额操作 - 保证余额一致性
  • 定时任务 - 防止重复执行
  • 数据同步 - 分布式环境下的数据一致性

总结

分布式锁是分布式系统中保证数据一致性的重要手段。选择合适的锁实现(Redis或ZooKeeper),合理设计锁粒度,正确处理超时和释放,才能构建可靠的高并发系统。