分布式锁原理与实现

动态演示分布式锁的工作机制与应用场景

分布式锁概述

分布式锁是控制分布式系统或不同进程对共享资源访问的一种锁实现,用于在分布式环境下保证互斥性。当多个进程不在同一个系统中,无法使用操作系统的锁机制时,需要一种跨进程的互斥手段来保持一致性。

分布式锁的特性

  • 互斥性:任意时刻只有一个客户端能持有锁
  • 避免死锁:即使持有锁的客户端崩溃,锁也能被释放
  • 高可用:锁服务要保证高可用,避免单点故障
  • 高性能:加锁、解锁操作要高效,不影响业务性能

应用场景

秒杀/库存扣减

防止超卖,确保库存准确性。多个服务实例同时处理下单请求时,需要对库存加锁以保证原子性。

分布式定时任务

集群环境下,确保定时任务只被一个节点执行,避免任务重复执行。

防重提交

防止表单或API的重复提交,尤其在支付场景下,保证幂等性。

业务流程控制

在某些业务流程中,需要保证同一时间只能有一个操作执行,如账务系统日终结算。

常见实现方式

基于Redis的分布式锁

利用Redis的原子操作特性实现分布式锁,具有高性能、实现简单的优点。


// 获取锁 SETNX + 过期时间
public boolean lock(String lockKey, String requestId, int expireTime) {
    // 使用SETNX命令尝试获取锁
    // SET key value NX PX milliseconds
    String result = jedis.set(lockKey, requestId, SetParams.setParams().nx().px(expireTime));
    return "OK".equals(result);
}

// 释放锁 Lua脚本保证原子性
public boolean unlock(String lockKey, String requestId) {
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then "
                  + "return redis.call('del', KEYS[1]) "
                  + "else return 0 end";
    Object result = jedis.eval(script, 
                              Collections.singletonList(lockKey),
                              Collections.singletonList(requestId));
    return Long.valueOf(1L).equals(result);
}
                        

基于ZooKeeper的分布式锁

利用ZooKeeper的临时顺序节点特性实现分布式锁,具有高可靠性、自动释放的优点。


// 创建分布式锁
public class ZookeeperDistributedLock {
    private ZooKeeper zk;
    private String lockPath;
    private String currentNode;
    private CountDownLatch connectedLatch = new CountDownLatch(1);
    
    // 获取锁
    public boolean lock() throws Exception {
        // 创建临时顺序节点
        currentNode = zk.create(lockPath + "/lock_", new byte[0], 
                                ZooDefs.Ids.OPEN_ACL_UNSAFE, 
                                CreateMode.EPHEMERAL_SEQUENTIAL);
        
        // 获取所有子节点
        List children = zk.getChildren(lockPath, false);
        Collections.sort(children); // 按照顺序排序
        
        // 如果是第一个节点,则获取锁成功
        String firstNode = children.get(0);
        if (currentNode.endsWith(firstNode)) {
            return true;
        }
        
        // 否则监听前一个节点
        String prevNode = firstNode;
        for (String node : children) {
            if (currentNode.endsWith(node)) {
                break;
            }
            prevNode = node;
        }
        
        CountDownLatch latch = new CountDownLatch(1);
        // 监听前一个节点的删除事件
        Stat stat = zk.exists(lockPath + "/" + prevNode, event -> {
            if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
                latch.countDown();
            }
        });
        
        // 如果前一个节点不存在,说明已经释放,获取锁成功
        if (stat == null) {
            return true;
        }
        
        // 等待前一个节点释放锁
        latch.await();
        return true;
    }
    
    // 释放锁
    public void unlock() throws Exception {
        if (currentNode != null) {
            zk.delete(currentNode, -1);
            currentNode = null;
        }
    }
}
                        

基于数据库的分布式锁

利用数据库的行锁或唯一索引特性实现分布式锁,简单直接但性能较低。


// 方法1:基于唯一索引
public boolean lock(String lockKey, String requestId, long expireTime) {
    try {
        // 创建一个包含唯一键约束的记录
        String sql = "INSERT INTO distributed_lock(lock_key, request_id, expire_time) VALUES(?, ?, ?)";
        int inserted = jdbcTemplate.update(sql, lockKey, requestId, System.currentTimeMillis() + expireTime);
        return inserted > 0;
    } catch (Exception e) {
        // 插入失败,说明锁已存在
        return false;
    }
}

// 方法2:基于行锁
public boolean lock(String lockKey, String requestId, long expireTime) {
    String sql = "SELECT * FROM distributed_lock WHERE lock_key = ? FOR UPDATE";
    List> result = jdbcTemplate.queryForList(sql, lockKey);
    
    if (result.isEmpty()) {
        // 锁不存在,创建锁
        jdbcTemplate.update("INSERT INTO distributed_lock(lock_key, request_id, expire_time) VALUES(?, ?, ?)", 
                          lockKey, requestId, System.currentTimeMillis() + expireTime);
        return true;
    } else {
        // 检查锁是否过期
        long expireAt = Long.parseLong(result.get(0).get("expire_time").toString());
        if (System.currentTimeMillis() > expireAt) {
            // 锁已过期,更新锁
            jdbcTemplate.update("UPDATE distributed_lock SET request_id = ?, expire_time = ? WHERE lock_key = ?", 
                              requestId, System.currentTimeMillis() + expireTime, lockKey);
            return true;
        }
    }
    return false;
}

// 释放锁
public boolean unlock(String lockKey, String requestId) {
    String sql = "DELETE FROM distributed_lock WHERE lock_key = ? AND request_id = ?";
    int deleted = jdbcTemplate.update(sql, lockKey, requestId);
    return deleted > 0;
}
                        

面临的挑战

死锁问题

当客户端获取锁后崩溃或网络中断,可能导致锁无法释放,造成死锁。解决方案包括设置锁超时、使用看门狗机制延长锁时间等。

性能问题

频繁的锁操作会增加系统负载,尤其在高并发场景下。优化方案包括使用本地缓存减少锁操作、锁粒度优化等。

可靠性问题

在分布式环境中,网络延迟、时钟偏移等因素可能导致锁失效。解决方案包括Redlock算法、强一致性分布式协调服务等。

动态演示

方案对比

实现方式 优点 缺点 适用场景
Redis
  • 实现简单
  • 性能高
  • 可设置过期时间自动释放
  • 单点Redis可靠性不高
  • 锁超时设置不当可能导致锁失效
高性能要求的场景,对锁可靠性要求不是特别高的业务
ZooKeeper
  • 可靠性高,具有强一致性
  • 自动释放机制(临时节点)
  • 支持阻塞等待
  • 实现复杂
  • 性能相对较低
  • 需要额外维护ZooKeeper集群
对一致性要求高的场景,需要阻塞等待功能的业务
数据库
  • 实现最简单
  • 利用现有数据库系统
  • 易于理解和排查问题
  • 性能最差
  • 行锁占用数据库连接
  • 可能产生额外的数据库负担
并发量低,对性能要求不高的场景,或者已经有事务操作的情况
Redlock
  • 高可靠性
  • 避免单点故障
  • 保证正确性
  • 实现复杂
  • 需要多个独立的Redis节点
  • 性能比单Redis略低
对锁的可靠性要求高,同时需要较好性能的场景