分布式锁详解:从数据库实现到中间件选型

云计算 分布式
分布式锁是分布式系统中的一个基础组件,选择合适的实现方案需要考虑:性能要求;可靠性要求;开发维护成本;团队技术栈。

引言

在分布式系统中,我们经常需要对共享资源进行互斥访问。比如:

  • 防止商品超卖
  • 避免重复下单
  • 确保任务只被处理一次
  • 保护共享资源不被并发修改

这就需要一个分布式锁机制。与单机环境下的线程锁不同,分布式锁需要在多个服务实例间生效,这带来了新的挑战。

图片图片

分布式锁的核心要求

一个可靠的分布式锁必须满足以下要求:

  1. 互斥性

在任意时刻,只能有一个客户端持有锁

不能出现多个客户端同时持有锁的情况

  1. 可重入性

同一个客户端可以多次获取同一把锁

需要维护锁的重入计数

  1. 防死锁

客户端崩溃时,锁必须能自动释放

锁必须有过期机制

  1. 高可用

锁服务不能成为系统瓶颈

锁服务必须保证高可用

基于数据库的实现

图片图片

1. 悲观锁实现

最简单的方式是利用数据库的行锁:

-- 创建锁表
CREATE TABLE distributed_lock (
    lock_key VARCHAR(50) PRIMARY KEY,
    lock_value VARCHAR(50),
    version INT,
    expire_time TIMESTAMP
);

-- 获取锁
SELECT * FROM distributed_lock 
WHERE lock_key = 'order_lock' 
FOR UPDATE;

Java 实现示例:

@Service
public class DatabaseDistributedLock {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Transactional
    public boolean acquireLock(String lockKey, String lockValue, long expireSeconds) {
        try {
            // 使用 FOR UPDATE 加锁查询
            String sql = "SELECT * FROM distributed_lock " +
                        "WHERE lock_key = ? FOR UPDATE";
            
            List<Map<String, Object>> result = jdbcTemplate.queryForList(
                sql, lockKey
            );
            
            if (result.isEmpty()) {
                // 锁不存在,创建锁
                jdbcTemplate.update(
                    "INSERT INTO distributed_lock " +
                    "(lock_key, lock_value, version, expire_time) " +
                    "VALUES (?, ?, 1, ?)",
                    lockKey,
                    lockValue,
                    LocalDateTime.now().plusSeconds(expireSeconds)
                );
                return true;
            }
            
            // 检查锁是否过期
            Map<String, Object> lock = result.get(0);
            LocalDateTime expireTime = ((Timestamp) lock.get("expire_time"))
                .toLocalDateTime();
                
            if (expireTime.isBefore(LocalDateTime.now())) {
                // 锁已过期,更新锁
                jdbcTemplate.update(
                    "UPDATE distributed_lock " +
                    "SET lock_value = ?, version = version + 1, expire_time = ? " +
                    "WHERE lock_key = ?",
                    lockValue,
                    LocalDateTime.now().plusSeconds(expireSeconds),
                    lockKey
                );
                return true;
            }
            
            return false;
        } catch (Exception e) {
            return false;
        }
    }
}

2. 乐观锁实现

使用版本号实现乐观锁:

@Service
public class OptimisticLock {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    public boolean acquireLock(String lockKey, String lockValue, int version) {
        int updated = jdbcTemplate.update(
            "UPDATE distributed_lock " +
            "SET lock_value = ?, version = version + 1 " +
            "WHERE lock_key = ? AND version = ?",
            lockValue,
            lockKey,
            version
        );
        
        return updated > 0;
    }
}

数据库实现的优缺点:

  • 优点:

实现简单

容易理解

不需要额外组件

  • 缺点:

性能较差

数据库压力大

无法优雅处理锁超时

基于 Redis 的实现

1. 单节点实现

使用 Redis 的 SETNX 命令:

@Service
public class RedisDistributedLock {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public boolean acquireLock(String lockKey, String lockValue, long expireSeconds) {
        return redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
    }
    
    public boolean releaseLock(String lockKey, String lockValue) {
        // 使用 Lua 脚本确保原子性
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "return redis.call('del', KEYS[1]) " +
            "else " +
            "return 0 " +
            "end";
            
        return redisTemplate.execute(
            new DefaultRedisScript<>(script, Boolean.class),
            Collections.singletonList(lockKey),
            lockValue
        );
    }
}

2. RedLock 算法

在 Redis 集群环境下,使用 RedLock 算法:

public class RedLock {
    
    private final List<StringRedisTemplate> redisList;
    private final int quorum;  // 大多数节点数
    
    public boolean acquireLock(String lockKey, String lockValue, long expireMillis) {
        int acquiredLocks = 0;
        long startTime = System.currentTimeMillis();
        
        // 尝试在每个节点上获取锁
        for (StringRedisTemplate redis : redisList) {
            if (tryAcquireLock(redis, lockKey, lockValue, expireMillis)) {
                acquiredLocks++;
            }
        }
        
        // 计算获取锁消耗的时间
        long elapsedTime = System.currentTimeMillis() - startTime;
        long remainingTime = expireMillis - elapsedTime;
        
        // 判断是否获取到足够的锁
        if (acquiredLocks >= quorum && remainingTime > 0) {
            return true;
        } else {
            // 释放所有获取的锁
            releaseLocks(lockKey, lockValue);
            return false;
        }
    }
    
    private boolean tryAcquireLock(
        StringRedisTemplate redis, 
        String lockKey, 
        String lockValue, 
        long expireMillis
    ) {
        try {
            return redis.opsForValue()
                .setIfAbsent(lockKey, lockValue, expireMillis, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            return false;
        }
    }
}

Redis 实现的优缺点:

  • 优点:

性能高

实现相对简单

支持自动过期

  • 缺点:

需要额外维护 Redis 集群

RedLock 算法实现复杂

时钟依赖问题

基于 ZooKeeper 的实现

图片图片

利用 ZooKeeper 的临时节点机制:

public class ZookeeperDistributedLock {
    
    private final CuratorFramework client;
    private final String lockPath;
    
    public boolean acquireLock(String lockKey) throws Exception {
        // 创建临时节点
        String path = lockPath + "/" + lockKey;
        try {
            client.create()
                .creatingParentsIfNeeded()
                .withMode(CreateMode.EPHEMERAL)
                .forPath(path);
            return true;
        } catch (NodeExistsException e) {
            return false;
        }
    }
    
    public void releaseLock(String lockKey) throws Exception {
        String path = lockPath + "/" + lockKey;
        client.delete().forPath(path);
    }
    
    // 实现可重入锁
    public class ReentrantZookeeperLock {
        private final ThreadLocal<Integer> lockCount = new ThreadLocal<>();
        
        public boolean acquire() throws Exception {
            Integer count = lockCount.get();
            if (count != null && count > 0) {
                // 入
                lockCount.set(count + 1);
                return true;
            }
            
            if (acquireLock("lock")) {
                lockCount.set(1);
                return true;
            }
            return false;
        }
        
        public void release() throws Exception {
            Integer count = lockCount.get();
            if (count == null) {
                return;
            }
            
            count--;
            if (count > 0) {
                lockCount.set(count);
            } else {
                lockCount.remove();
                releaseLock("lock");
            }
        }
    }
}

ZooKeeper 实现的优缺点:

  • 优点:

可靠性高

自动释放锁

支持监听机制

  • 缺点:

性能一般

实现复杂

需要维护 ZooKeeper 集群

业务场景分析

1. 秒杀场景

场景特点:

  • 并发量极高
  • 时间窗口集中
  • 对性能要求极高
  • 数据一致性要求高

推荐方案: Redis + Lua脚本

@Service
public class SeckillLockService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public boolean trySecKill(String productId, String userId) {
        // Lua脚本保证原子性
        String script = 
            "if redis.call('exists', KEYS[1]) == 0 then " +
            "  redis.call('set', KEYS[1], ARGV[1]) " +
            "  redis.call('decrby', KEYS[2], 1) " +
            "  return 1 " +
            "end " +
            "return 0";
            
        List<String> keys = Arrays.asList(
            "seckill:lock:" + productId + ":" + userId,
            "seckill:stock:" + productId
        );
        
        return redisTemplate.execute(
            new DefaultRedisScript<>(script, Boolean.class),
            keys,
            "1"
        );
    }
}

原因分析:

  • Redis 的高性能满足并发要求
  • Lua 脚本保证原子性
  • 内存操作速度快
  • 集群方案保证可用性

2. 定时任务场景

场景特点:

  • 多实例部署
  • 任务不能重复执行
  • 故障转移需求
  • 实时性要求不高

推荐方案: ZooKeeper

public class ScheduledTaskLock {
    
    private final CuratorFramework client;
    
    public void executeTask() {
        String taskPath = "/scheduled-tasks/daily-report";
        try {
            // 创建临时节点
            client.create()
                .creatingParentsIfNeeded()
                .withMode(CreateMode.EPHEMERAL)
                .forPath(taskPath);
                
            try {
                // 执行任务
                generateDailyReport();
            } finally {
                // 删除节点
                client.delete().forPath(taskPath);
            }
        } catch (NodeExistsException e) {
            // 其他实例正在执行
            log.info("Task is running on other instance");
        }
    }
}

原因分析:

  • ZooKeeper 的临时节点特性保证故障时自动释放锁
  • 强一致性保证任务不会重复执行
  • Watch 机制便于监控任务执行状态

3. 订单支付场景

场景特点:

  • 并发量适中
  • 数据一致性要求高
  • 需要事务支持
  • 有业务回滚需求

推荐方案: 数据库行锁 + 事务

@Service
public class PaymentLockService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Transactional
    public boolean processPayment(String orderId, BigDecimal amount) {
        // 使用 FOR UPDATE 锁定订单记录
        String sql = "SELECT * FROM orders WHERE order_id = ? FOR UPDATE";
        Map<String, Object> order = jdbcTemplate.queryForMap(sql, orderId);
        
        // 检查订单状态
        if (!"PENDING".equals(order.get("status"))) {
            return false;
        }
        
        // 执行支付逻辑
        jdbcTemplate.update(
            "UPDATE orders SET status = 'PAID' WHERE order_id = ?",
            orderId
        );
        
        // 记录支付流水
        jdbcTemplate.update(
            "INSERT INTO payment_log (order_id, amount) VALUES (?, ?)",
            orderId, amount
        );
        
        return true;
    }
}

原因分析:

  • 数据库事务保证数据一致性
  • 行锁防止并发支付
  • 便于与其他业务集成
  • 支持事务回滚

4. 库存扣减场景

场景特点:

  • 并发量较高
  • 需要预占库存
  • 需要处理超时释放
  • 对性能要求较高

推荐方案: Redis + 延时队列

@Service
public class InventoryLockService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public boolean lockInventory(String productId, int quantity, String orderId) {
        // 加锁并预占库存
        String script = 
            "local stock = redis.call('get', KEYS[1]) " +
            "if stock and tonumber(stock) >= tonumber(ARGV[1]) then " +
            "  redis.call('decrby', KEYS[1], ARGV[1]) " +
            "  redis.call('setex', KEYS[2], 1800, ARGV[1]) " +
            "  return 1 " +
            "end " +
            "return 0";
            
        List<String> keys = Arrays.asList(
            "inventory:" + productId,
            "inventory:lock:" + orderId
        );
        
        boolean locked = redisTemplate.execute(
            new DefaultRedisScript<>(script, Boolean.class),
            keys,
            String.valueOf(quantity)
        );
        
        if (locked) {
            // 添加延时释放任务
            redisTemplate.opsForZSet().add(
                "inventory:timeout",
                orderId,
                System.currentTimeMillis() + 1800000
            );
        }
        
        return locked;
    }
}

原因分析:

  • Redis 的高性能满足并发要求
  • 延时队列处理超时释放
  • 原子操作保证数据一致性
  • 便于扩展和监控

实现方案对比

特性

数据库

Redis

ZooKeeper

性能

可靠性

实现复杂度

维护成本

自动释放

需要额外实现

支持

支持

可重入性

需要额外实现

需要额外实现

需要额外实现

最佳实践

  1. 选择建议

简单场景:使用数据库实现

高性能要求:使用 Redis 实现

高可靠要求:使用 ZooKeeper 实现

  1. 实现建议

设置合理的超时时间

实现可重入机制

添加监控和告警

做好日志记录

  1. 使用建议

缩小锁的粒度

减少锁的持有时间

避免死锁

做好异常处理

结论

分布式锁是分布式系统中的一个基础组件,选择合适的实现方案需要考虑:

  • 性能要求
  • 可靠性要求
  • 开发维护成本
  • 团队技术栈

没有最好的方案,只有最合适的方案。在实际应用中,要根据具体场景选择合适的实现方式。

正文内容从这里开始(可直接省略,亦可配图说明)。

责任编辑:武晓燕 来源: 架构成长指南
相关推荐

2021-11-14 16:07:35

中间件阿里Seata

2021-11-26 06:43:19

Java分布式

2019-08-12 11:00:59

美团网MySQL数据库

2012-11-30 10:21:46

移动中间件

2019-08-19 10:24:33

分布式事务数据库

2017-11-27 05:06:42

数据库中间件cobar

2017-11-27 05:36:16

数据库中间件TDDL

2024-05-07 07:58:10

数据架构大数据中间件架构

2017-12-01 05:04:32

数据库中间件Atlas

2018-02-24 19:37:33

Java8数据库中间件

2024-12-06 08:29:29

2017-05-23 18:55:05

mysql-proxy数据库架构

2011-08-10 13:03:58

CJDBC数据库集群

2015-06-16 10:39:43

NoSQL分布式算法

2022-08-01 18:33:45

关系型数据库大数据

2017-12-04 09:00:00

金融开源软件分布式消息中间件

2022-07-21 07:31:41

数据库分布式

2017-07-26 09:41:28

MyCATSQLMongoDB

2024-05-06 00:00:00

.NET分布式锁技术

2021-03-11 09:53:07

SpringBoot数据库分布式锁
点赞
收藏

51CTO技术栈公众号