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

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

1.分布式锁要求

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

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

2.实现方案

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

  1. 基于数据库实现的分布式锁:可以通过数据库的乐观锁或悲观锁实现分布式锁,但是由于数据库的 IO 操作比较慢,不适合高并发场景。
  2. 基于 ZooKeeper 实现的分布式锁:ZooKeeper 是一个高可用性的分布式协调服务,可以通过它来实现分布式锁。但是使用 ZooKeeper 需要部署额外的服务,增加了系统复杂度。
  3. 基于 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 中,使用 <update> 标签定义更新语句,同时使用 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 步:

  1. 引入 Curator 和 ZooKeeper 客户端依赖;
  2. 配置 ZooKeeper 连接信息;
  3. 编写分布式锁实现类。

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 实现分布式锁,它的实现步骤如下:

  1. 添加 Redisson 依赖
  2. 配置 Redisson 连接信息
  3. 编写分布式锁代码类

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 编写分布式锁代码类

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 需要部署额外的服务,增加了系统复杂度。所以没有最好的解决方案,只有最合适自己的解决方案。

特殊说明

以上内容来自我的《Java 面试突击训练营》,这门课程是有着十几年工作经验(前 360 开发工程师),10 年面试官经验的我,花费 4 年时间打磨完成的一门视频面试课。学完训练营的课程之后,基本可以应对目前市面上绝大部分公司的面试了,并且课程配备了 9 大就业服务,帮助上千人找到 Java 工作,其中上百人拿到大厂 Offer,学员最高薪资 70W 年薪,面试课目录和 9 大服务如下:

加我微信咨询:vipStone【备注:训练营】