基于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

相关推荐
剩下了什么13 小时前
MySQL JSON_SET() 函数
数据库·mysql·json
山峰哥13 小时前
数据库工程与SQL调优——从索引策略到查询优化的深度实践
数据库·sql·性能优化·编辑器
较劲男子汉13 小时前
CANN Runtime零拷贝传输技术源码实战 彻底打通Host与Device的数据传输壁垒
运维·服务器·数据库·cann
java搬砖工-苤-初心不变13 小时前
MySQL 主从复制配置完全指南:从原理到实践
数据库·mysql
心态还需努力呀14 小时前
CANN仓库通信库:分布式训练的梯度压缩技术
分布式·cann
山岚的运维笔记15 小时前
SQL Server笔记 -- 第18章:Views
数据库·笔记·sql·microsoft·sqlserver
roman_日积跬步-终至千里16 小时前
【LangGraph4j】LangGraph4j 核心概念与图编排原理
java·服务器·数据库
汇智信科16 小时前
打破信息孤岛,重构企业效率:汇智信科企业信息系统一体化运营平台
数据库·重构
野犬寒鸦16 小时前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
indexsunny17 小时前
互联网大厂Java面试实战:Spring Boot微服务在电商场景中的应用与挑战
java·spring boot·redis·微服务·kafka·spring security·电商