Redis分布式锁

为什么需要分布式锁

在多线程环境中,如果多个线程同时访问共享资源(例如商品库存等秒杀场景下),会发生数据竞争,可能会导致出现脏数据或者系统问题,威胁到程序的正常运行。

我的项目中为了保证共享资源被安全的访问,使用了互斥锁对资源进行了保护,即同一时刻只有一个线程访问并修改资源,其他线程需要等到该线程释放才能去访问。

我们传统的单机多线程情况下:

Synchronized锁是锁本地的,属于JVM级别的(只能解决同一个jvm下多线程间的互斥),如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。因此我们引进了分布式锁。

分布式锁的定义

满足分布式系统下多进程可见并且互斥的锁。多个服务器(tomcat)之间可见的锁。

使用场景:同一个数据只能被修改一次,或者秒杀【数据的变化是需要被感知】

分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程一起进行,让程序串行执行。

分布式锁底层主要用了redis的setnx命令:由于Redis是单线程的,用了setnx命令之后,只有一个客户端对某一个key设置值,在没有过期或删除key的时候其他客户端是不能设置这个key的。

setnx

Java代码中为setIfAbsent。这个命令在Redis中用于仅在指定的键不存在的时候设置键的值。如果键已存在,则不会执行任何操作,并且返回0表示设置失败。如果键不存在,则会设置新的值,并返回1表示设置成功。,

①防止误删其他锁,需要加个判断的逻辑,同时为了保证原子性

为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。

选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

②为了防止在删除锁的时候,服务器突然宕机,导致锁一直不存在,我们需要加个过期时间

具体怎么实现的?

我们通过UUID来区分不同JVM发来的请求,这样将来在删除锁之前进行判断的时候就不会误删

java 复制代码
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
   // 获取线程标示
   String threadId = ID_PREFIX + Thread.currentThread().getId();
   // 获取锁
   Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
   return Boolean.TRUE.equals(success);
}

删除锁

java 复制代码
public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标示是否一致
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

setnx底层实现过程

Redis使用一个哈希表来存储键值对,当执行setnx命令时,Redis会去哈希表中查找指定的键。如果键存在,则返回0,如果键不存在则Redis会创建一个新的键值对,并将值与键关联起来。


通过添加过期时间做为兜底方案还不够保险,如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。但如果锁的超时时间设置过长,又会影响到性能。


引出了Redisson框架

分布式锁中如何合理控制锁的有效时长

的确,这个时间是通过setnx直接指定的。不能过长也不能过短。

因此我们当时采用的是redis的一个框架redisson实现的 给锁续期(看门狗机制)

尽可能保证锁是业务完成之后自己释放的,而不是因为过期而释放的。

当锁住的一个业务还没有执行完成的时候,在redisson中引入一个看门狗机制。每隔一段时间就检查当前业务是否还持有锁,如果持有则增加当前业务持有锁的时间,当业务执行完成之后则释放锁就可以了。

同时,在高并发条件下,一个业务有可能会执行很快,客户1持有锁的时候,客户2来了也不会马上拒绝,它会不断尝试获取锁,如果客户1释放之后,客户2也能马上持有锁,性能也能得到提升。

相关推荐
~我爱敲代码~10 小时前
使用XSHELL远程操作数据库
数据库·adb
资深web全栈开发15 小时前
如何正确使用缓存:常见陷阱与最佳实践
redis·缓存·golang
春风霓裳16 小时前
sql-窗口函数
大数据·数据库·sql
言之。16 小时前
【数据库】TiDB 技术选型与架构分析报告
数据库·架构·tidb
人工智能训练17 小时前
如何在 Ubuntu 22.04 中安装 Docker 引擎和 Linux 版 Docker Desktop 桌面软件
linux·运维·服务器·数据库·ubuntu·docker·ai编程
胖头鱼的鱼缸(尹海文)17 小时前
数据库管理-第386期 使用OCP部署OceanBase 4.4.1社区版集群(20251107)
数据库·oceanbase
Craaaayon17 小时前
如何选择两种缓存更新策略(写缓存+异步写库;写数据库+异步更新缓存)
java·数据库·redis·后端·缓存·mybatis
一 乐17 小时前
点餐|智能点餐系统|基于java+ Springboot的动端的点餐系统小程序(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·小程序·论文
WarriorTan17 小时前
理解PostgreSQL中的数据块
数据库·postgresql
学好statistics和DS17 小时前
三个好思路:SQL并行化处理、混淆矩阵和特征交叉
数据库·sql·矩阵