【分布式锁】它是什么?怎么用?

为什么需要分布式锁?主要功能是什么?核心目的是什么?

为什么需要分布式锁?

在分布式系统中,多个服务实例需要访问共享资源(如数据库、缓存、文件等)。如果没有协调机制,并发操作会导致:

  1. 数据不一致:多个节点同时修改同一数据
  2. 重复处理:多个节点执行相同任务(如重复扣款)
  3. 资源竞争:如超卖问题(库存被多个请求同时扣减)

主要功能

  1. 互斥性(Mutual Exclusion):同一时刻只有一个客户端能持有锁
  2. 避免死锁(Deadlock Prevention):锁必须有超时机制
  3. 容错性(Fault Tolerance):即使持有锁的客户端崩溃,锁也能自动释放
  4. 高可用(High Availability):锁服务本身需保证高可用

核心目的

确保分布式环境下对共享资源的原子性访问,防止并发操作导致的数据不一致问题。


分布式锁实现方式及代码示例(含详细注释和使用案例)


方式1:基于Redis实现

原理:利用Redis的原子操作(SETNX + EXPIRE)

Maven依赖 (pom.xml)
<!-- Redis客户端依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件 (application.yml)
spring:
  redis:
    host: localhost    # Redis服务器地址
    port: 6379         # Redis端口
    password:          # 密码(没有则留空)
    timeout: 5000ms    # 连接超时时间
Java代码实现(含详细注释)
@Component
public class RedisDistributedLock {
    
    @Autowired
    private StringRedisTemplate redisTemplate;  // Spring提供的Redis操作模板

    /**
     * 尝试获取分布式锁
     * @param lockKey    锁的key
     * @param clientId   客户端唯一标识(可使用UUID)
     * @param expireSeconds 锁过期时间(秒)
     * @return true-获取成功,false-获取失败
     */
    public boolean tryLock(String lockKey, String clientId, long expireSeconds) {
        // 使用SET key value NX EX命令保证原子性:
        // NX: 仅当key不存在时设置
        // EX: 设置过期时间(秒)
        return redisTemplate.opsForValue().setIfAbsent(
            lockKey, 
            clientId, 
            Duration.ofSeconds(expireSeconds)  // 设置过期时间
        );
    }

    /**
     * 释放分布式锁
     * @param lockKey   锁的key
     * @param clientId  客户端唯一标识
     * @return true-释放成功,false-释放失败
     */
    public boolean unlock(String lockKey, String clientId) {
        // 使用Lua脚本保证检查锁和删除锁的原子性
        String script = 
            // 检查锁是否属于当前客户端
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +  // 属于则删除
            "else " +
            "   return 0 " +  // 不属于返回0
            "end";
        
        // 创建Redis脚本对象
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        
        // 执行脚本
        Long result = redisTemplate.execute(
            redisScript, 
            Collections.singletonList(lockKey),  // KEYS[1]
            clientId                            // ARGV[1]
        );
        
        // 结果1表示删除成功,0表示失败
        return result != null && result == 1;
    }
}
使用案例:商品库存扣减
@Service
public class ProductService {
    
    @Autowired
    private RedisDistributedLock redisLock;
    
    @Autowired
    private ProductRepository productRepo;
    
    private static final String STOCK_LOCK_PREFIX = "lock:product:stock:";
    
    /**
     * 扣减商品库存(使用Redis分布式锁)
     * @param productId 商品ID
     * @param quantity  扣减数量
     * @return 扣减结果
     */
    public boolean reduceStock(Long productId, int quantity) {
        String lockKey = STOCK_LOCK_PREFIX + productId;
        String clientId = UUID.randomUUID().toString(); // 唯一客户端标识
        boolean locked = false;
        
        try {
            // 尝试获取锁(等待1秒,锁有效期10秒)
            locked = redisLock.tryLock(lockKey, clientId, 10);
            if (!locked) {
                throw new RuntimeException("获取分布式锁失败");
            }
            
            // 查询商品库存
            Product product = productRepo.findById(productId)
                .orElseThrow(() -> new RuntimeException("商品不存在"));
            
            // 检查库存是否充足
            if (product.getStock() < quantity) {
                throw new RuntimeException("库存不足");
            }
            
            // 扣减库存
            product.setStock(product.getStock() - quantity);
            productRepo.save(product);
            
            return true;
        } finally {
            // 释放锁
            if (locked) {
                redisLock.unlock(lockKey, clientId);
            }
        }
    }
}

方式2:基于ZooKeeper实现

原理:利用临时顺序节点(Ephemeral Sequential Node)

Maven依赖 (pom.xml)
<!-- ZooKeeper客户端Curator框架 -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.1.0</version>
</dependency>
Java代码实现(含详细注释)
public class ZookeeperDistributedLock {
    private CuratorFramework client;  // ZooKeeper客户端
    private InterProcessMutex lock;   // 分布式锁对象

    /**
     * 构造函数(创建ZooKeeper连接)
     * @param connectString ZooKeeper连接地址
     */
    public ZookeeperDistributedLock(String connectString) {
        // 重试策略:初始间隔1秒,最多重试3次
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        // 创建客户端
        client = CuratorFrameworkFactory.newClient(
            connectString, 
            retryPolicy
        );
        client.start();  // 启动客户端
    }

    /**
     * 尝试获取锁
     * @param lockPath  锁路径(如:/locks/order)
     * @param waitTime  等待时间
     * @param unit      时间单位
     * @return true-获取成功,false-获取失败
     */
    public boolean tryLock(String lockPath, long waitTime, TimeUnit unit) throws Exception {
        // 创建互斥锁(底层使用临时顺序节点)
        lock = new InterProcessMutex(client, lockPath);
        // 尝试获取锁,设置等待时间
        return lock.acquire(waitTime, unit);
    }

    /**
     * 释放锁
     */
    public void unlock() throws Exception {
        // 检查当前进程是否持有锁
        if (lock != null && lock.isAcquiredInThisProcess()) {
            lock.release();  // 释放锁(自动删除临时节点)
        }
    }
}
使用案例:配置中心配置更新
@Service
public class ConfigService {
    
    private final ZookeeperDistributedLock zkLock;
    
    private static final String CONFIG_LOCK_PATH = "/locks/config/update";
    
    public ConfigService() {
        // 初始化ZooKeeper连接(实际项目中应使用@Value注入地址)
        this.zkLock = new ZookeeperDistributedLock("localhost:2181");
    }
    
    /**
     * 更新全局配置(使用ZooKeeper分布式锁)
     * @param config 新配置
     */
    public void updateGlobalConfig(Config config) {
        try {
            // 尝试获取锁(最多等待2秒)
            if (!zkLock.tryLock(CONFIG_LOCK_PATH, 2, TimeUnit.SECONDS)) {
                throw new RuntimeException("配置更新正在被其他节点执行");
            }
            
            // 执行配置更新操作
            System.out.println("开始更新全局配置...");
            Thread.sleep(1000); // 模拟耗时操作
            System.out.println("全局配置更新完成");
            
        } catch (Exception e) {
            throw new RuntimeException("配置更新失败", e);
        } finally {
            try {
                // 释放锁
                zkLock.unlock();
            } catch (Exception e) {
                // 记录日志
            }
        }
    }
}

方式3:基于数据库实现

原理:利用数据库唯一索引或乐观锁

表结构(MySQL示例)
CREATE TABLE distributed_lock (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    lock_key VARCHAR(128) NOT NULL UNIQUE,  -- 锁名称(唯一约束)
    client_id VARCHAR(64) NOT NULL,         -- 客户端ID
    expire_time TIMESTAMP NOT NULL          -- 过期时间
);
Java代码实现(含详细注释)
@Repository
public class DatabaseLockDao {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;  // Spring JDBC模板

    /**
     * 尝试获取锁
     * @param lockKey       锁名称
     * @param clientId      客户端ID
     * @param expireSeconds 锁过期时间(秒)
     * @return true-获取成功,false-获取失败
     */
    public boolean tryLock(String lockKey, String clientId, int expireSeconds) {
        try {
            // 使用INSERT ON DUPLICATE KEY UPDATE实现锁竞争
            int updated = jdbcTemplate.update(
                "INSERT INTO distributed_lock (lock_key, client_id, expire_time) " +
                "VALUES (?, ?, NOW() + INTERVAL ? SECOND) " +  // 插入新锁
                "ON DUPLICATE KEY UPDATE " +  // 当锁已存在时更新
                "   client_id = IF(expire_time < NOW(), VALUES(client_id), client_id), " +  // 如果过期则覆盖
                "   expire_time = IF(expire_time < NOW(), VALUES(expire_time), expire_time)", // 更新过期时间
                lockKey, clientId, expireSeconds
            );
            
            // 返回是否成功获取锁
            return updated > 0;
        } catch (DuplicateKeyException e) {
            // 锁已被其他客户端持有
            return false;
        }
    }

    /**
     * 释放锁
     * @param lockKey   锁名称
     * @param clientId  客户端ID
     */
    public void unlock(String lockKey, String clientId) {
        // 删除当前客户端持有的锁
        jdbcTemplate.update(
            "DELETE FROM distributed_lock WHERE lock_key = ? AND client_id = ?",
            lockKey, clientId
        );
    }
}
使用案例:定时任务调度
@Service
public class ScheduledTaskService {
    
    @Autowired
    private DatabaseLockDao dbLockDao;
    
    private static final String TASK_LOCK_KEY = "daily_report_generation";
    private static final String CLIENT_ID = "server-node-1"; // 通常使用主机名或IP
    
    /**
     * 生成每日报表(使用数据库分布式锁)
     */
    @Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
    public void generateDailyReport() {
        // 尝试获取锁(有效期30分钟)
        boolean locked = dbLockDao.tryLock(TASK_LOCK_KEY, CLIENT_ID, 1800);
        
        if (!locked) {
            System.out.println("其他节点正在执行报表生成任务");
            return;
        }
        
        try {
            System.out.println("开始生成每日报表...");
            // 模拟耗时操作
            Thread.sleep(5000);
            System.out.println("每日报表生成完成");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 释放锁
            dbLockDao.unlock(TASK_LOCK_KEY, CLIENT_ID);
        }
    }
}

三种实现方式对比

特性 Redis ZooKeeper 数据库
性能 ⭐⭐⭐⭐⭐ (10万+/秒) ⭐⭐ (1万/秒) ⭐ (500/秒)
实现复杂度 简单 中等 简单
可靠性 依赖Redis持久化 ⭐⭐⭐⭐⭐ (强一致性) 依赖数据库可靠性
锁自动释放 通过EXPIRE实现 Session断开自动删除临时节点 需额外实现超时清理
避免死锁 超时自动释放 Session断开自动释放 需手动清理过期锁
可重入性 需自行实现 原生支持 需自行实现
适用场景 高并发、短任务 强一致性要求场景 低并发、简单系统
缺点 集群需RedLock方案 性能较低、依赖ZooKeeper可用性 性能差、数据库压力大

分布式锁 vs 分布式事务

关系说明

  1. 互补关系

    • 分布式锁:解决短时资源互斥访问(如秒杀扣库存)
    • 分布式事务:解决跨服务的多个操作原子性(如银行转账)
  2. 协同工作场景

    graph LR A[订单服务] -->|锁定库存| B[分布式锁] A --> C[支付服务] C -->|扣款| D[分布式事务]
    • 步骤1:用分布式锁锁定商品库存(防止超卖)
    • 步骤2:通过分布式事务协调订单+支付操作(保证两者同时成功/失败)
  3. 关键区别

    维度 分布式锁 分布式事务
    目标 资源互斥访问 跨服务操作原子性
    持续时间 秒级(短时) 秒~分钟(长时)
    典型方案 Redis/ZooKeeper Seata/TCC/Saga
    数据一致性级别 最终一致性 强一致性

总结

  • 分布式锁是分布式事务的前置保障(确保资源安全),但不是事务的替代品。
  • 在复杂业务中,通常先加锁保护关键资源,再通过分布式事务完成跨服务操作

典型应用场景:电商下单流程

  1. 使用Redis分布式锁锁定商品库存(防止超卖)
  2. 通过Seata分布式事务同时创建订单和扣减账户余额
  3. 若事务失败则释放锁并回滚所有操作
posted @ 2025-06-12 15:59  佛祖让我来巡山  阅读(49)  评论(0)    收藏  举报

佛祖让我来巡山博客站 - 创建于 2018-08-15

开发工程师个人站,内容主要是网站开发方面的技术文章,大部分来自学习或工作,部分来源于网络,希望对大家有所帮助。

Bootstrap中文网