基于Redis的分布式锁

1 实现原理

利用 Redis 的原子操作特性

  • Redis 是单线程处理命令的数据库,这使得它的一些操作具有原子性。在分布式锁的实现中,主要利用SET命令的原子性来实现锁的获取。
  • 例如,使用SET key value NX PX milliseconds命令,其中NX(Not eXists)参数表示只有当键key不存在时才设置成功,PX参数用于设置键的过期时间(以毫秒为单位)。这就保证了在多个客户端同时请求获取锁时,只有一个客户端能够成功设置键值对,从而实现了互斥性。

设置过期时间避免死锁

  • 为了防止客户端在获取锁之后由于某种原因(如进程崩溃、网络故障等)无法释放锁,导致其他客户端永远无法获取锁的情况(死锁),在获取锁时会为锁设置一个过期时间。
  • 当锁过期后,Redis 会自动删除这个键值对,使得其他客户端有机会获取锁。过期时间的合理设置非常重要,需要根据业务逻辑的执行时间来确定,一般要保证业务逻辑能够在过期时间内完成。

通过唯一标识验证锁的归属

  • 每个客户端在获取锁时会生成一个唯一标识(如使用UUID)作为value存储在 Redis 中。在释放锁时,需要验证当前锁对应的value是否与自己当初设置的一致,只有一致时才能释放锁。
  • 这是因为在分布式环境中,可能会出现锁过期后被其他客户端重新获取的情况,如果不进行验证,可能会导致一个客户端误删其他客户端获取的锁。

2 代码实现

通过 set key value px milliseconds nx 命令实现加锁, 通过Lua脚本实现解锁。

javascript 复制代码
//获取锁(unique_value可以是UUID等)
SET resource_name unique_value NX PX  30000

//释放锁(lua脚本中,一定要比较value,防止误解锁)
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
  • set 命令要用 set key value px milliseconds nx,替代 setnx + expire 需要分两次执行命令的方式,保证了原子性,
  • value 要具有唯一性,可以使用UUID.randomUUID().toString()方法生成,用来标识这把锁是属于哪个请求加的,在解锁的时候就可以有依据;
  • 释放锁时要验证 value 值,防止误解锁;
  • 通过 Lua 脚本来避免 Check And Set 模型的并发问题,因为在释放锁的时候因为涉及到多个Redis操作 (利用了eval命令执行Lua脚本的原子性);

依赖

首先,我们需要引入Redis的客户端依赖。这里以Spring Data Redis为例:

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

工具类

我们可以创建一个Redis工具类来封装锁的操作:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class RedisLock {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String LOCK_KEY_PREFIX = "lock:";
    private static final String UNLOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    public boolean tryLock(String lockKey, String requestId, long expireTime, TimeUnit timeUnit) {
        String result = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY_PREFIX + lockKey, requestId, expireTime, timeUnit);
        return result != null;
    }

    public boolean unlock(String lockKey, String requestId) {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(UNLOCK_LUA_SCRIPT);
        redisScript.setResultType(Long.class);
        return redisTemplate.execute(redisScript, Collections.singletonList(LOCK_KEY_PREFIX + lockKey), requestId) == 1L;
    }
}

锁的获取和释放

使用上述工具类,我们可以轻松地获取和释放锁:

java 复制代码
public class RedisLockExample {

    @Autowired
    private RedisLock redisLock;

    public void someMethod() {
        String lockKey = "someLockKey";
        String requestId = UUID.randomUUID().toString();
        boolean locked = redisLock.tryLock(lockKey, requestId, 10, TimeUnit.SECONDS);

        if (locked) {
            try {
                // 执行需要同步的代码
            } finally {
                redisLock.unlock(lockKey, requestId);
            }
        } else {
            // 获取锁失败,执行其他逻辑
        }
    }
}

3 Redisson锁的续期

当使用上述的实现方法时,如果获取锁后,【业务没执行完,锁过期释放】,此时该如何解决?

4 Redlock

相关推荐
欣慰的三叶草(● ̄(エ) ̄●)6 分钟前
Navicat 17 for Mac 数据库管理软件
数据库·macos·数据库管理·navicat·数据库连接工具·navicat17·mysql连接
Greyscarf27 分钟前
SQL Server 数据库 忘记密码
数据库
森森淼淼丶28 分钟前
oceanbase集群访问异常问题处理
运维·数据库·oceanbase
阿年、嗯啊32 分钟前
MySQL和Hive中的行转列、列转行
数据库·hive·mysql·侧窗·行专列、列转行·hive侧窗列转行·构造map数据结构
清风xu来1 小时前
Docker 环境中搭建 Redis 哨兵模式集群的步骤与问题解决
redis·docker·容器·sentinel·redis哨兵
tatasix1 小时前
Redis 实现分布式锁
数据库·redis·分布式
高铭杰1 小时前
Postgresql中clog与xid对应关系计算方法(速查表)
数据库·postgresql·clog·xid
鸠摩智首席音效师1 小时前
如何备份和恢复 PostgreSQL 数据库 ?
数据库·postgresql
码农君莫笑2 小时前
SQL中聚类后字段数据串联字符串方法研究
数据库·sql
幽兰的天空2 小时前
在C#中,如何使用委托实现事件处理?
前端·数据库·c#