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

相关推荐
寂寞旅行2 小时前
向量数据库Milvus的使用
数据库·milvus
闻哥2 小时前
Redis事务详解
java·数据库·spring boot·redis·缓存·面试
道亦无名3 小时前
aiPbMgrSendAck
java·网络·数据库
面向对象World5 小时前
正点原子Mini Linux 4.3寸800x480触摸屏gt115x驱动
linux·服务器·数据库
dinga198510266 小时前
mysql之联合索引
数据库·mysql
洛豳枭薰6 小时前
分布式事务进阶
分布式
微风中的麦穗6 小时前
【SQL Server 2019】企业级数据库系统—数据库SQL Server 2019保姆级详细图文下载安装完全指南
大数据·数据库·sqlserver·云计算·个人开发·运维必备·sqlserver2019
无心水6 小时前
5、微服务快速启航:基于Pig与BladeX构建高可用分布式系统实战
服务器·分布式·后端·spring·微服务·云原生·架构
zjttsh7 小时前
MySQL加减间隔时间函数DATE_ADD和DATE_SUB的详解
android·数据库·mysql
顾北127 小时前
SpringCloud 系列 04:Gateway 断言 / 过滤器 / 限流 一站式落地指南
java·开发语言·数据库