【必会面试题】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接口提供了多种锁操作,包括公平锁、可重入锁等高级特性,极大地简化了分布式锁的使用。

相关推荐
白云如幻几秒前
MySQL的分组函数
数据库·mysql
荒川之神16 分钟前
ORACLE 闪回技术简介
数据库·oracle
ZHOU西口1 小时前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
zmd-zk1 小时前
kafka+zookeeper的搭建
大数据·分布式·zookeeper·中间件·kafka
时差9532 小时前
【面试题】Hive 查询:如何查找用户连续三天登录的记录
大数据·数据库·hive·sql·面试·database
让学习成为一种生活方式2 小时前
R包下载太慢安装中止的解决策略-R语言003
java·数据库·r语言
秋意钟2 小时前
MySQL日期类型选择建议
数据库·mysql
Dxy12393102163 小时前
python下载pdf
数据库·python·pdf
桀桀桀桀桀桀4 小时前
数据库中的用户管理和权限管理
数据库·mysql
superman超哥5 小时前
04 深入 Oracle 并发世界:MVCC、锁、闩锁、事务隔离与并发性能优化的探索
数据库·oracle·性能优化·dba