分布式锁的3种实现!附代码

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云原生网关 MSE Higress,422元/月
简介: 分布式锁的3种实现!附代码

分布式锁是一种用于保证分布式系统中多个进程或线程同步访问共享资源的技术。同时它又是面试中的常见问题,所以我们本文就重点来看分布式锁的具体实现(含实现代码)。

在分布式系统中,由于各个节点之间的网络通信延迟、故障等原因,可能会导致数据不一致的问题。分布式锁通过协调多个节点的行为,保证在任何时刻只有一个节点可以访问共享资源,以避免数据的不一致性和冲突。

1.分布式锁要求

分布式锁通常需要满足以下几个要求:

  1. 互斥性:在任意时刻只能有一个客户端持有锁。
  2. 不会发生死锁:即使持有锁的客户端发生故障,也能保证锁最终会被释放。
  3. 具有容错性:分布式锁需要能够容忍节点故障等异常情况,保证系统的稳定性。

    2.实现方案

    在 Java 中,实现分布式锁的方案有多种,包括:

  4. 基于数据库实现的分布式锁:可以通过数据库的乐观锁或悲观锁实现分布式锁,但是由于数据库的 IO 操作比较慢,不适合高并发场景。

  5. 基于 ZooKeeper 实现的分布式锁:ZooKeeper 是一个高可用性的分布式协调服务,可以通过它来实现分布式锁。但是使用 ZooKeeper 需要部署额外的服务,增加了系统复杂度。
  6. 基于 Redis 实现的分布式锁:Redis 是一个高性能的内存数据库,支持分布式部署,可以通过Redis的原子操作实现分布式锁,而且具有高性能和高可用性。

    3.数据库分布式锁

    数据库的乐观锁或悲观锁都可以实现分布式锁,下面分别来看。

    3.1 悲观锁

    在数据库中使用 for update 关键字可以实现悲观锁,我们在 Mapper 中添加 for update 即可对数据加锁,实现代码如下:

    <!-- UserMapper.xml -->
    <select id="selectByIdForUpdate" resultType="User">
     SELECT * FROM user WHERE id = #{id} FOR UPDATE
    </select>
    

    在 Service 中调用 Mapper 方法,即可获取到加锁的数据:

    @Transactional
    public void updateWithPessimisticLock(int id, String name) {
         
     User user = userMapper.selectByIdForUpdate(id);
     if (user != null) {
         
         user.setName(name);
         userMapper.update(user);
     } else {
         
         throw new RuntimeException("数据不存在");
     }
    }
    

    3.2 乐观锁

    在 MyBatis 中,可以通过给表添加一个版本号字段来实现乐观锁。在 Mapper 中,使用 标签定义更新语句,同时使用 set 标签设置版本号的增量。

    <!-- UserMapper.xml -->
    <update id="updateWithOptimisticLock">
     UPDATE user SET
     name = #{
         name},
     version = version + 1
     WHERE id = #{
         id} AND version = #{
         version}
    </update>
    

    在 Service 中调用 Mapper 方法,需要传入更新数据的版本号。如果更新失败,说明数据已经被其他事务修改,具体实现代码如下:

    @Transactional
    public void updateWithOptimisticLock(int id, String name, int version) {
         
     User user = userMapper.selectById(id);
     if (user != null) {
         
         user.setName(name);
         user.setVersion(version);
         int rows = userMapper.updateWithOptimisticLock(user);
         if (rows == 0) {
         
             throw new RuntimeException("数据已被其他事务修改");
         }
     } else {
         
         throw new RuntimeException("数据不存在");
     }
    }
    

    4.Zookeeper 分布式锁

    在 Spring Boot 中,可以使用 Curator 框架来实现 ZooKeeper 分布式锁,具体实现分为以下 3 步:

  7. 引入 Curator 和 ZooKeeper 客户端依赖;

  8. 配置 ZooKeeper 连接信息;
  9. 编写分布式锁实现类。

    4.1 引入 Curator 和 ZooKeeper

    <dependency>
     <groupId>org.apache.curator</groupId>
     <artifactId>curator-framework</artifactId>
     <version>latest</version>
    </dependency>
    <dependency>
     <groupId>org.apache.curator</groupId>
     <artifactId>curator-recipes</artifactId>
     <version>latest</version>
    </dependency>
    <dependency>
     <groupId>org.apache.zookeeper</groupId>
     <artifactId>zookeeper</artifactId>
     <version>latest</version>
    </dependency>
    

    4.2 配置 ZooKeeper 连接

    在 application.yml 中添加 ZooKeeper 连接配置:

    spring:
    zookeeper:
     connect-string: localhost:2181
     namespace: demo
    

    4.3 编写分布式锁实现类

    @Component
    public class DistributedLock {
         
    
     @Autowired
     private CuratorFramework curatorFramework;
    
     /**
      * 获取分布式锁
      *
      * @param lockPath   锁路径
      * @param waitTime   等待时间
      * @param leaseTime  锁持有时间
      * @param timeUnit   时间单位
      * @return 锁对象
      * @throws Exception 获取锁异常
      */
     public InterProcessMutex acquire(String lockPath, long waitTime, long leaseTime, TimeUnit timeUnit) throws Exception {
         
         InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
         if (!lock.acquire(waitTime, timeUnit)) {
         
             throw new RuntimeException("获取分布式锁失败");
         }
         if (leaseTime > 0) {
         
             lock.acquire(leaseTime, timeUnit);
         }
         return lock;
     }
    
     /**
      * 释放分布式锁
      *
      * @param lock 锁对象
      * @throws Exception 释放锁异常
      */
     public void release(InterProcessMutex lock) throws Exception {
         
         if (lock != null) {
         
             lock.release();
         }
     }
    }
    

    5.Redis 分布式锁

    我们可以使用 Redis 客户端 Redisson 实现分布式锁,它的实现步骤如下:

  10. 添加 Redisson 依赖

  11. 配置 Redisson 连接信息
  12. 编写分布式锁代码类

    5.1 添加 Redisson 依赖

    在 pom.xml 中添加如下配置:
    <!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
    <dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson-spring-boot-starter</artifactId>
     <version>3.20.0</version>
    </dependency>
    

    5.2 配置 Redisson 连接

    在 Spring Boot 项目的配置文件 application.yml 中添加 Redisson 配置:
    spring:
    data:
     redis:
       host: localhost
       port: 6379
       database: 0
    redisson:
    codec: org.redisson.codec.JsonJacksonCodec
    single-server-config:
     address: "redis://${spring.data.redis.host}:${spring.redis.port}"
     database: "${spring.data.redis.database}"
     password: "${spring.data.redis.password}"
    

    5.3 编写分布式锁代码类

    ```java
    import jakarta.annotation.Resource;
    import org.redisson.Redisson;
    import org.redisson.api.RLock;
    import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class RedissonLockService {
@Resource
private Redisson redisson;

/**
 * 加锁
 *
 * @param key     分布式锁的 key
 * @param timeout 超时时间
 * @param unit    时间单位
 * @return
 */
public boolean tryLock(String key, long timeout, TimeUnit unit) {
    RLock lock = redisson.getLock(key);
    try {
        return lock.tryLock(timeout, unit);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return false;
    }
}

/**
 * 释放分布式锁
 *
 * @param key 分布式锁的 key
 */
public void unlock(String key) {
    RLock lock = redisson.getLock(key);
    lock.unlock();
}

}
```

6.Redis VS Zookeeper

Redis 和 ZooKeeper 都可以用来实现分布式锁,它们在实现分布式锁的机制和原理上有所不同,具体区别如下:

  1. 数据存储方式:Redis 将锁信息存储在内存中,而 ZooKeeper 将锁信息存储在 ZooKeeper 的节点上,因此 ZooKeeper 需要更多的磁盘空间。
  2. 锁的释放:Redis 的锁是通过设置锁的过期时间来自动释放的,而 ZooKeeper 的锁需要手动释放,如果锁的持有者出现宕机或网络中断等情况,需要等待锁的超时时间才能自动释放。
  3. 锁的竞争机制:Redis 使用的是单机锁,即所有请求都直接连接到同一台 Redis 服务器,容易发生单点故障;而 ZooKeeper 使用的是分布式锁,即所有请求都连接到 ZooKeeper 集群,具有较好的可用性和可扩展性。
  4. 一致性:Redis 的锁是非严格意义下的分布式锁,因为在多台机器上运行多个进程时,由于 Redis 的主从同步可能会存在数据不一致的问题;而 ZooKeeper 是强一致性的分布式系统,保证了数据的一致性。
  5. 性能:Redis 的性能比 ZooKeeper 更高,因为 Redis 将锁信息存储在内存中,而 ZooKeeper 需要进行磁盘读写操作。

总之,Redis 适合实现简单的分布式锁场景,而 ZooKeeper 适合实现复杂的分布式协调场景,也就是 ZooKeeper 适合强一致性的分布式系统。

强一致性是指系统中的所有节点在任何时刻看到的数据都是一致的。ZooKeeper 中的数据是有序的树形结构,每个节点都有唯一的路径标识符,所有节点都共享同一份数据,当任何一个节点对数据进行修改时,所有节点都会收到通知,更新数据,并确保数据的一致性。
在 ZooKeeper 中,强一致性体现在数据的读写操作上。ZooKeeper 使用 ZAB(ZooKeeper Atomic Broadcast)协议来保证数据的一致性,该协议确保了数据更新的顺序,所有的数据更新都需要经过集群中的大多数节点确认,保证了数据的一致性和可靠性。

小结

在 Java 中,使用数据库、ZooKeeper 和 Redis 都可以实现分布式锁。但数据库 IO 操作比较慢,不适合高并发场景;Redis 执行效率最高,但在主从切换时,可能会出现锁丢失的情况;ZooKeeper 是一个高可用性的分布式协调服务,可以保证数据的强一致性,但是使用 ZooKeeper 需要部署额外的服务,增加了系统复杂度。所以没有最好的解决方案,只有最合适自己的解决方案。

本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

相关文章
|
缓存 监控 算法
如何进行移动应用的性能优化?
*移动应用性能优化涉及:减少数据传输和压缩资源,优化内存管理和代码,使用异步处理,监控性能,调整数据库和网络请求,进行测试与调试,以及提升用户体验。优化是持续过程,需多角度策略并持续改进。*
291 3
|
消息中间件 数据库 RocketMQ
分布式事务常见解决方案
分布式事务常见解决方案
1680 0
|
SQL 关系型数据库 数据库
学习分布式事务Seata看这一篇就够了,建议收藏
学习分布式事务Seata看这一篇就够了,建议收藏
17342 2
|
5月前
|
设计模式 SQL 安全
并发设计模式实战系列(13):双重检查锁定(Double-Checked Locking)
🌟 大家好,我是摘星!🌟今天为大家带来的是并发设计模式实战系列,第十三章,废话不多说直接开始~
245 0
|
6月前
|
存储 安全 Java
ThreadLocal - 原理与应用场景详解
ThreadLocal是Java中用于实现线程隔离的重要工具,为每个线程提供独立的变量副本,避免多线程数据共享带来的安全问题。其核心原理是通过 ThreadLocalMap 实现键值对存储,每个线程维护自己的存储空间。ThreadLocal 广泛应用于线程隔离、跨层数据传递、复杂调用链路的全局参数传递及数据库连接管理等场景。此外,InheritableThreadLocal 支持子线程继承父线程的变量值,而 TransmittableThreadLocal 则解决了线程池中变量传递的问题,提升了多线程上下文管理的可靠性。深入理解这些机制,有助于开发者更好地解决多线程环境下的数据隔离与共享挑战。
1166 43
|
5月前
|
关系型数据库 MySQL Java
安装和配置JDK、Tomcat、MySQL环境,以及如何在Linux下更改后端端口。
遵循这些步骤,你可以顺利完成JDK、Tomcat、MySQL环境的安装和配置,并在Linux下更改后端端口。祝你顺利!
359 11
|
7月前
|
负载均衡 Dubbo Java
Spring Cloud Alibaba与Spring Cloud区别和联系?
Spring Cloud Alibaba与Spring Cloud区别和联系?
|
消息中间件 Linux API
centos7 安装rabbitmq自定义版本及配置
centos7 安装rabbitmq自定义版本及配置
|
缓存 NoSQL 数据库
分布式锁三种实现方式及对比
分布式锁三种实现方式及对比
401 0
|
11月前
|
消息中间件 数据库
Seata框架的工作原理
你还可以进一步深入研究 Seata 框架的技术细节和具体实现,以更好地理解其工作原理和优势。同时,结合实际应用场景进行实践和优化,也是提高分布式事务处理能力的重要途径。
470 15