【必会面试题】Redis如何实现分布式锁?

目录

一、为什么要使用分布式锁?

为了解决分布式系统中并发控制和资源同步的问题。

  • 传统的单机系统:使用如Java中的synchronized关键字或ReentrantLock等机制来实现线程间的同步,确保同一时间只有一个线程能访问共享资源。
  • 分布式系统环境:服务被部署在多台机器上,传统的本地锁机制无法跨越不同的JVM或服务器进程,因此需要引入分布式锁来保证在分布式环境下的资源访问控制。

二、什么是分布式锁?

分布式锁是一种在分布式系统中协调多进程或线程对共享资源进行访问控制的机制。它确保同一时刻只有一个进程或线程能够访问特定资源,从而防止并发操作导致的数据不一致问题。在分布式环境中,由于系统分布在不同的网络节点上,传统的单机锁机制不再适用,因此需要引入分布式锁。

三、什么是Redis分布式锁?

Redis分布式锁是利用Redis作为中间件实现的一种分布式锁机制。Redis凭借其高性能、高可用性和丰富的数据结构特性,成为实现分布式锁的理想选择。通过Redis提供的命令,如SETNX(Set if Not eXists)、GETSETEXPIRE等,可以构建出既简单又高效的锁机制。

四、如何实现Redis分布式锁?

当然,让我们深入探讨如何使用 Redis 实现分布式锁,特别是通过几种典型方法,并强调每个方法的关键细节和最佳实践。

1. 基础方法:SETNX + EXPIRE

这是实现分布式锁最基础的方式,利用 Redis 的 SETNX 命令尝试设置一个键值对,如果键不存在则设置成功,表示获取锁成功。同时,为了防止锁持有者崩溃导致的死锁,需要设置一个过期时间(使用 EXPIREPEXPIRE 命令)。

问题 :这种方法的问题在于 SETNXEXPIRE 是两个独立的命令(即非原子性的),如果在这两个操作之间程序崩溃,可能会导致锁没有设置过期时间,造成死锁。
解决:从 Redis 2.6.12 版本开始,SET 命令支持在一次操作中完成设置键值、设置过期时间和检查键是否存在,所以生产环境中推荐使用SET命令的NX和PX参数来实现原子性设置值和过期时间。

sql 复制代码
SET my_lock_key "lock_value" NX PX 5000

2. Lua 脚本

为了解决 SETNXEXPIRE的安全性问题,也可以使用 Lua 脚本将设置锁和设置过期时间的操作封装在一个原子操作中,保证执行 Lua 脚本的原子性。

Lua 脚本示例

lua 复制代码
-- KEYS[1] 是锁的键名
-- ARGV[1] 是锁的过期时间,单位为毫秒
-- ARGV[2] 是一个随机值,作为锁的标识符,防止误删其他客户端的锁

if redis.call("setnx", KEYS[1], ARGV[2]) == 1 then
    redis.call("pexpire", KEYS[1], ARGV[1])
    return 1 -- 返回1表示获取锁成功
else
    return 0 -- 返回0表示获取锁失败
end

使用方法 :客户端通过 EVALEVALSHA 命令执行上述脚本,传递锁的键、值和过期时间。

3. Redisson 实现

Redisson 是一个高级的 Redis 客户端,它内置了对分布式锁的支持,提供了丰富的锁特性,如可重入、公平锁、锁自动续期等。

示例代码

java 复制代码
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");

RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");

try {
    lock.lock(); // 可以传入等待时间和leaseTime等参数
    // 执行业务代码
} finally {
    lock.unlock();
}

Redisson 自动处理了锁的超时、重入和释放等问题,使得使用起来更加安全和便捷。

4. Redlock 算法

Redlock 算法由 Redis 的作者提出,旨在提供一个更安全的分布式锁实现。它要求客户端在多个独立的 Redis 节点上尝试获取锁,只有当大多数节点都同意时才认为获取成功。

步骤

  1. 客户端从 N 个独立的 Redis 节点中选出 N 个(通常 N >= 5),并尝试在每个节点上获取锁。
  2. 客户端计算获取锁成功与失败的比例,如果比例超过半数(且获取锁的节点数至少为 N/2 + 1),则认为成功获取锁。
  3. 如果获取锁成功,客户端计算所有锁的最小过期时间,并以此作为实际锁的有效时间。
  4. 在操作完成后,客户端向所有Redis实例释放锁。即使在某个节点上解锁失败,只要之前成功解锁了大多数节点,就可以认为锁已经被正确释放。

注意事项

  • 实际部署时,确保Redis实例分布在不同的物理机器上,减少共因失效的风险。
  • 需要权衡实例数量和性能、复杂性之间的关系,过多的实例会增加锁获取的复杂度和网络延迟。
  • 虽然Redlock在理论上很吸引人,但在某些情况下其安全性仍然存在争议,特别是关于网络分区的处理上。

五、Java+Redis分布式锁

在Java中使用Redis实现分布式锁,一个常见的做法是利用Jedis客户端直接操作Redis命令,或者使用客户端库如Redisson。

1. 基于Jedis的简单实现

首先,确保你的项目中已经添加了Jedis的依赖。

Maven依赖

xml 复制代码
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

Java代码示例

java 复制代码
import redis.clients.jedis.Jedis;

public class DistributedLockWithJedis {
    private Jedis jedis;
    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;

    public DistributedLockWithJedis(String host, int port) {
        jedis = new Jedis(host, port);
    }
	//使用 SET 命令的 NX 和 PX 来获取锁
    public boolean lock(String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime * 1000);
        return LOCK_SUCCESS.equals(result);
    }
	//使用Lua脚本来确保只有锁的持有者才能删除锁
    public boolean unlock(String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, 1, lockKey, requestId);
        return RELEASE_SUCCESS.equals(result);
    }
}

lock方法尝试使用SET命令的NX(Only set the key if it does not already exist)和PX(Set the specified expire time, in milliseconds)选项来获取锁,同时设置锁的持有者和过期时间。unlock方法使用Lua脚本来确保只有锁的持有者才能删除锁,提高了操作的原子性。

2. 使用Redisson实现

Redisson是一个为Redis设计的Java客户端,提供了许多高级特性,包括分布式锁。

Maven依赖

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>

Java代码示例

java 复制代码
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;

public class DistributedLockWithRedisson {
    private RedissonClient redisson;

    public DistributedLockWithRedisson(String address) {
        Config config = new Config();
        config.useSingleServer().setAddress(address);
        redisson = Redisson.create(config);
    }

    public void lockAndUnlock(String lockKey) {
        RLock lock = redisson.getLock(lockKey);
        try {
            lock.lock();
            // 执行业务代码
            System.out.println("Locked and processing...");
        } finally {
            lock.unlock();
            System.out.println("Unlocked.");
        }
    }
}

Redisson内部实现了锁的获取、续期、释放等方法,只需要调用简单的API即可。RLock接口提供了多种锁操作,包括公平锁、可重入锁等高级特性,极大地简化了分布式锁的使用。

相关推荐
小宋102135 分钟前
玩转RabbitMQ声明队列交换机、消息转换器
服务器·分布式·rabbitmq
小安运维日记2 小时前
Linux云计算 |【第四阶段】NOSQL-DAY1
linux·运维·redis·sql·云计算·nosql
kejijianwen2 小时前
JdbcTemplate常用方法一览AG网页参数绑定与数据寻址实操
服务器·数据库·oracle
编程零零七3 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
高兴就好(石6 小时前
DB-GPT部署和试用
数据库·gpt
这孩子叫逆6 小时前
6. 什么是MySQL的事务?如何在Java中使用Connection接口管理事务?
数据库·mysql
Karoku0666 小时前
【网站架构部署与优化】web服务与http协议
linux·运维·服务器·数据库·http·架构
懒洋洋的华3696 小时前
消息队列-Kafka(概念篇)
分布式·中间件·kafka
码农郁郁久居人下7 小时前
Redis的配置与优化
数据库·redis·缓存
March€7 小时前
分布式事务的基本实现
分布式