分布式锁

简介: 分布式锁是分布式系统中实现跨节点资源互斥访问的关键机制,常用于解决多进程、多机器环境下的并发控制问题。它依赖外部存储(如Redis、ZooKeeper)协调锁状态,确保全局唯一性和原子性操作。常见实现包括基于Redis的单点锁与RedLock算法、ZooKeeper的临时顺序节点及数据库唯一索引。适用于任务调度、缓存重建和库存管理等场景。设计时需关注可重入性、锁超时、续租及异常处理,并权衡性能与可靠性。

分布式锁是在分布式系统中实现跨节点资源互斥访问的一种机制,用于解决多进程、多机器环境下的并发控制问题。与单机锁(如synchronized)不同,分布式锁需要依赖外部共享存储(如Redis、ZooKeeper)来协调不同节点间的锁状态。

核心原理

  1. 全局唯一性
    通过共享存储(如Redis的SETNX命令)确保同一时间只有一个客户端能获取锁。

  2. 原子性操作
    锁的获取和释放必须是原子操作,避免竞态条件。例如:

    # Redis的SET命令同时设置值和过期时间(原子操作)
    SET lock_key "value" NX PX 30000  # 30秒过期
    
  3. 锁超时机制
    防止锁持有者崩溃后锁无法释放,通过设置过期时间自动失效。

分布式锁的实现方式

1. 基于Redis的实现

  • 单点Redis

    import redis
    import time
    
    class RedisLock:
        def __init__(self, redis_client, lock_key, expire_time=30):
            self.redis = redis_client
            self.lock_key = lock_key
            self.expire_time = expire_time
            self.uuid = str(uuid.uuid4())  # 唯一标识锁持有者
    
        def acquire(self):
            return self.redis.set(self.lock_key, self.uuid, nx=True, ex=self.expire_time)
    
        def release(self):
            script = """
            if redis.call("get", KEYS[1]) == ARGV[1] then
                return redis.call("del", KEYS[1])
            else
                return 0
            end
            """
            return self.redis.eval(script, 1, self.lock_key, self.uuid)
    
  • RedLock算法(多节点Redis):
    向多个独立的Redis节点同时获取锁,超过半数成功则认为获取成功,提高可靠性。

2. 基于ZooKeeper的实现

  • 创建临时顺序节点,最小节点持有者获得锁,其他节点监听前一个节点的删除事件。

    public class ZkLock implements AutoCloseable {
         
        private final CuratorFramework client;
        private final String lockPath;
        private String currentPath;
        private String previousPath;
    
        public ZkLock(CuratorFramework client, String lockPath) {
         
            this.client = client;
            this.lockPath = lockPath;
        }
    
        public void acquire() throws Exception {
         
            if (currentPath == null) {
         
                currentPath = client.create()
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                    .forPath(lockPath + "/lock-");
            }
    
            List<String> children = client.getChildren().forPath(lockPath);
            Collections.sort(children);
    
            if (currentPath.endsWith(children.get(0))) {
         
                return; // 当前节点是最小节点,获取锁成功
            }
    
            // 监听前一个节点
            previousPath = lockPath + "/" + children.get(children.indexOf(currentPath.substring(lockPath.length() + 1)) - 1);
            CountDownLatch latch = new CountDownLatch(1);
            NodeCache cache = new NodeCache(client, previousPath);
            cache.getListenable().addListener(() -> {
         
                if (cache.getCurrentData() == null) {
         
                    latch.countDown();
                }
            });
            cache.start();
    
            // 检查前一个节点是否已删除
            if (client.checkExists().forPath(previousPath) == null) {
         
                return;
            }
    
            latch.await(); // 等待前一个节点释放锁
        }
    
        @Override
        public void close() throws Exception {
         
            client.delete().forPath(currentPath);
        }
    }
    

3. 基于数据库的实现

  • 通过唯一索引或INSERT ... ON DUPLICATE KEY UPDATE实现:

    -- 创建锁表
    CREATE TABLE distributed_lock (
        lock_key VARCHAR(64) PRIMARY KEY,
        expire_time DATETIME NOT NULL
    );
    
    -- 获取锁
    INSERT INTO distributed_lock (lock_key, expire_time)
    VALUES ('resource_key', NOW() + INTERVAL 30 SECOND)
    ON DUPLICATE KEY UPDATE expire_time = NOW() + INTERVAL 30 SECOND;
    

分布式锁的典型应用场景

  1. 分布式任务调度
    防止多个节点同时执行同一任务。

  2. 缓存失效重建
    避免缓存击穿时大量请求同时重建缓存。

  3. 库存扣减
    保证分布式系统中库存操作的原子性。

分布式锁的设计要点

  1. 可重入性
    同一客户端可多次获取同一把锁,需记录获取次数。

  2. 锁的续租
    通过定时任务延长锁的过期时间,防止任务执行时间过长导致锁提前释放。

  3. 异常处理

    • 锁持有者崩溃时,通过过期时间自动释放锁。
    • 释放锁时验证锁的所有者,防止误释放。

与单机锁的对比

特性 单机锁 分布式锁
作用范围 同一进程内 跨进程、跨机器
实现方式 JVM内置(如synchronized) 依赖外部存储(Redis、ZooKeeper)
可靠性 取决于外部存储的可靠性
性能 高(无网络开销) 低(网络调用开销)
复杂度 高(需处理网络分区等)

注意事项

  1. 网络分区问题
    当发生网络分区时,可能导致多个节点同时认为自己持有锁(如Redis的脑裂问题)。

  2. 锁的粒度
    避免使用全局锁,尽量细化锁的粒度以提高并发度。

  3. 性能权衡
    分布式锁引入网络开销,需根据业务场景选择合适的实现方案。

总结

分布式锁是分布式系统中实现资源互斥的关键机制,常用Redis、ZooKeeper或数据库实现。设计时需考虑原子性、可重入性、锁超时和异常处理等问题。在选择实现方案时,需根据业务场景权衡性能、可靠性和复杂度。合理使用分布式锁能有效避免多节点并发带来的数据一致性问题。

目录
相关文章
|
10月前
|
Java Spring 容器
SpringBoot自动配置的原理是什么?
Spring Boot自动配置核心在于@EnableAutoConfiguration注解,它通过@Import导入配置选择器,加载META-INF/spring.factories中定义的自动配置类。这些类根据@Conditional系列注解判断是否生效。但Spring Boot 3.0后已弃用spring.factories,改用新格式的.imports文件进行配置。
1334 0
|
10月前
|
消息中间件 canal 存储
如何解决并发环境下双写不一致的问题?
在并发环境下,“双写不一致”指数据库与缓存因操作顺序或执行时机差异导致数据不匹配。解决核心是保证操作的原子性、顺序性或最终一致性。常见方案包括延迟双删、加锁机制、binlog同步、版本号机制和读写锁分离,分别适用于不同一致性要求和并发场景,需根据业务需求综合选择。
762 0
|
JSON 前端开发 Java
深入理解 Spring Boot 中日期时间格式化:@DateTimeFormat 与 @JsonFormat 完整实践
在 Spring Boot 开发中,日期时间格式化是前后端交互的常见痛点。本文详细解析了 **@DateTimeFormat** 和 **@JsonFormat** 两个注解的用法,分别用于将前端传入的字符串解析为 Java 时间对象,以及将时间对象序列化为指定格式返回给前端。通过完整示例代码,展示了从数据接收、业务处理到结果返回的全流程,并总结了解决时区问题和全局配置的最佳实践,助你高效处理日期时间需求。
2037 0
|
10月前
|
JSON Java 数据格式
Spring Boot返回Json数据及数据封装
在Spring Boot中,接口间及前后端的数据传输通常使用JSON格式。通过@RestController注解,可轻松实现Controller返回JSON数据。该注解是Spring Boot新增的组合注解,结合了@Controller和@ResponseBody的功能,默认将返回值转换为JSON格式。Spring Boot底层默认采用Jackson作为JSON解析框架,并通过spring-boot-starter-json依赖集成了相关库,包括jackson-databind、jackson-datatype-jdk8等常用模块,简化了开发者对依赖的手动管理。
826 3
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
718 29
JVM简介—1.Java内存区域
|
存储 关系型数据库 索引
什么是聚簇索引及其优缺点?
聚簇索引并不是单独的索引类型,而是一种数据存储方式。 B+树索引分为聚簇索引和非聚簇索引,主键索引就是聚簇索引的一种,非聚簇索引有复合索引、前缀索引、唯一索引。 在innodb存储引擎中,表数据本身就是按B+树组织的一个索引结构,聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚簇索引的叶子节点成为数据页。 Innodb通过主键聚集数据,如果没有定义主键,innodb会选择非空的唯一索引代替。如果没有这样的索引,innodb会隐式的定义一个主键来作为聚簇索引。 非聚簇索引又称为辅助索引,InnoDB访问数据需要两次查找,辅助索引叶子节点存储的不再是行
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
解决IDEA中 Could not autowire. No beans of 'xxxx' type found 的错误提示
解决IDEA中 Could not autowire. No beans of 'xxxx' type found 的错误提示
13053 3
解决IDEA中 Could not autowire. No beans of 'xxxx' type found 的错误提示
|
消息中间件 存储 负载均衡
2024消息队列“四大天王”:Rabbit、Rocket、Kafka、Pulsar巅峰对决
本文对比了 RabbitMQ、RocketMQ、Kafka 和 Pulsar 四种消息队列系统,涵盖架构、性能、可用性和适用场景。RabbitMQ 以灵活路由和可靠性著称;RocketMQ 支持高可用和顺序消息;Kafka 专为高吞吐量和低延迟设计;Pulsar 提供多租户支持和高可扩展性。性能方面,吞吐量从高到低依次为
6803 1
|
Java Maven 开发工具
【IntelliJ IDEA】使用Maven方式构建Spring Boot Web 项目(超详细)1
【IntelliJ IDEA】使用Maven方式构建Spring Boot Web 项目(超详细)
1143 2