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也能马上持有锁,性能也能得到提升。

相关推荐
Rookie也要加油33 分钟前
01_SQLite
数据库·sqlite
liuxin3344556637 分钟前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
看山还是山,看水还是。1 小时前
MySQL 管理
数据库·笔记·mysql·adb
fishmemory7sec1 小时前
Koa2项目实战2(路由管理、项目结构优化)
数据库·mongodb·koa
momo小菜pa2 小时前
【MySQL 09】表的内外连接
数据库·mysql
Jasonakeke2 小时前
【重学 MySQL】四十九、阿里 MySQL 命名规范及 MySQL8 DDL 的原子化
数据库·mysql
程序猿小D2 小时前
第二百六十九节 JPA教程 - JPA查询OrderBy两个属性示例
java·开发语言·数据库·windows·jpa
小宇成长录2 小时前
Mysql:数据库和表增删查改基本语句
数据库·mysql·数据库备份
极客先躯3 小时前
高级java每日一道面试题-2024年10月3日-分布式篇-分布式系统中的容错策略都有哪些?
java·分布式·版本控制·共识算法·超时重试·心跳检测·容错策略
团儿.3 小时前
解锁MySQL高可用新境界:深入探索MHA架构的无限魅力与实战部署
数据库·mysql·架构·mysql之mha架构