003 redis分布式锁 jedis分布式锁 Redisson分布式锁 分段锁

文章目录

在同一个JVM内部,大家往往采用synchronized或者Lock的方式来解决多线程间的安全问题,但是在分布式架构下,在JVM之间,那么就需要一种更加高级的锁机制,来处理这种跨JVM进程之间的线程安全问题,解决方案就是:使用分布式锁。

Redis分布式锁原理

Redis分布式锁机制,主要借助setnx和expire两个命令完成

setnx:当key不存在,将key设置为value,存在不做任何操作,返回0

客户端如果宕机,锁谁也加不上,即死锁。当持有锁的客户端宕机时,它可能没有机会释放锁,导致其他客户端无法获取锁。

expire:设置key过期时间

原理解析:

1key不存在时创建,并设置value和过期时间,返回值为1;成功获取到锁

2如果key存在时直接返回0,抢锁失败

3持有锁的线程释放锁时,手动删除key;或者过期时间到,key自动删除,锁释放

加锁的问题

setnx成功

expire失败

如果没有手动释放,那么这个锁永远被占用,其他线程永远也抢不到锁

解决方案:

1.使用set的命令时,同时设置过期时间

命令:set lock '123' EX 100 NX

SET lock_key unique_value NX PX 30000

NX 表示只有当 key 不存在时才设置它。

PX 30000 表示 key 的过期时间为 30,000 毫秒(即 30 秒)

如果正在使用较旧版本的 Redis,或者出于某种原因需要使用 SETNX,可以考虑结合 EXPIRE 命令来手动为 key 设置过期时间。但这种方法的一个缺点是,在 SETNX 和 EXPIRE 之间存在一个小的时间窗口,其中如果客户端宕机,可能会导致 key 没有设置过期时间。因此,使用 SET 命令的 NX 和 PX 选项是更安全和推荐的方法。

java 复制代码
set 同时设置过期时间命令
set key value[EX seconds][PX milliseconds][NX|XX]

EX seconds:设置失效时长,单位秒
PX milliseconds设置失效时长,单位毫秒
NX key不存在时设置value,成功返回OK,失败返回nil
XX key存在时设置value,成功返回OK,失败返回nil

2.使用lua脚本,将加锁的命令放在lua脚本中原子性的执行

java 复制代码
EVAL:对Lua脚本进行求值,命令如下:
EVAL script numkeys key [key ...] arg [arg ...]


>eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
>1) "key1" 2)"key2" 3)"first" 4)"second"

1script:参数是一段Lua5.1脚本程序,它会被运行在Redis服务器上下文中

2numkeys:参数用于指定键名参数的个数

这个命令的含义如下:

EVAL 是执行 Lua 脚本的命令。

"return {KEYS1,KEYS2,ARGV1,ARGV2}" 是要执行的 Lua 脚本。这个脚本很简单,它只是返回一个包含四个元素的表(在 Lua 中,表是唯一的复合数据类型,类似于其他语言中的数组或字典)。这四个元素分别是脚本接收到的前两个 key 和前两个 argv。

2 是传递给 Lua 脚本的 key 的数量。这告诉 Redis,接下来的两个参数(key1 和 key2)应该被视为 key。

key1 和 key2 是传递给 Lua 脚本的两个 key。在 Lua 脚本中,它们可以通过 KEYS1 和 KEYS2 来访问。

first 和 second 是传递给 Lua 脚本的两个参数值。在 Lua 脚本中,它们可以通过 ARGV1 和 ARGV2 来访问。

所以,当你执行这个命令时,Lua 脚本会返回一个表,包含这四个值:key1, key2, first, second。

在Lua脚本中,可以使用redis.call()函数来执行Redis命令

java 复制代码
#这段脚本实现了将键stock的值设为no
>eval "return redis.call('set',KEYS[1],ARGV[1])" 1 stock no

Jedis分布式锁实现

加锁:就是调用SET key PX NX命令

java 复制代码
set key value [EX seconds] [PX milliseconds] [NX|XX]

key 加锁的key

value UUID.randomUUID().toString(),代表加锁的客户端请求标识

nxxx NX,表示SET IF NOT EXIST

expx PX,表示毫秒

pom.xml

xml 复制代码
<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.16.4</version>
        </dependency>

RedisCommandLock.java

java 复制代码
package com.example.demo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import redis.clients.jedis.Jedis;

import java.util.Collections;

@Slf4j
@Data
@AllArgsConstructor
public class RedisCommandLock {

    private RedisTemplate redisTemplate;

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static   boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

    /**
     * 最常见的解锁代码就是直接使用 jedis.del() 方法删除锁,
     * 这种不先判断锁的拥有者而直接解锁的方式,
     * 会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。
     * @param jedis
     * @param lockKey
     */
    public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
        jedis.del(lockKey);
    }

    /**
     * 这种解锁代码乍一看也是没问题,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:
     * 它首先检查锁的拥有者(通过 requestId.equals(jedis.get(lockKey))),然后如果条件满足,它会删除锁。但是,这两个操作(get 和 del)是分开的,不是原子的。这意味着,在检查锁的拥有者和删除锁之间,其他客户端可能已经更改了锁的状态。
     *
     * 具体来说,以下是一个可能的问题场景:
     *
     * 客户端A获取了锁,并在某个时间点尝试释放它。
     * 客户端A调用 jedis.get(lockKey) 来检查它是否仍然拥有锁。假设它仍然拥有锁。
     * 在客户端A执行 jedis.del(lockKey) 之前,另一个客户端B可能已经获取了该锁(因为客户端A还没有释放它)。
     * 客户端A继续执行 jedis.del(lockKey),此时它实际上删除了客户端B刚刚获取的锁。
     * @param jedis
     * @param lockKey
     * @param requestId
     */
    public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
        // 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 若在此时,这把锁突然不是这个客户端的,则会误解锁
            jedis.del(lockKey);
        }

    }

}

RedisCommandLockTest.java

java 复制代码
package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;

import java.util.UUID;

import static org.junit.jupiter.api.Assertions.*;

@Slf4j
public class RedisCommandLockTest {

    private Jedis jedis;

    @BeforeEach
    void setUp() {
        jedis = new Jedis("127.0.0.1", 6379);
//        jedis.auth("123456");
    }

    @Test
    public void testTryGetDistributedLock() {
        boolean result = RedisCommandLock.tryGetDistributedLock(jedis, "test:lock", UUID.randomUUID().toString(), 300000);
        System.out.println(result);
    }

    @Test
    public void testReleaseDistributedLock() {
        boolean result = RedisCommandLock.releaseDistributedLock(jedis, "test:lock", "3c8feabb-befd-4552-805f-6a78bc7c43b4");
        System.out.println(result);
    }



}

锁过期问题

预估业务操作10秒,锁设置20秒,各种原因,比如STW问题,业务操作执行超过20秒,业务会在无锁状态下运行,就会发生数据紊乱

注:STW:Java中Stop-The-World机制简称STW,常发送于fullGC,这时Java应用程序的其他所有线程都被挂起(除了垃圾收集器之外)

1乐观锁方式,增加版本号(增加版本号需要调整业务逻辑,与之配合,所以会入侵代码)

2watch do,自动延期(不会侵入业务代码,redisson就是采用这种方案)

客户端1加锁的key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁

只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间

Redisson分布式锁

Redisson是基于Netty的Redis客户端。不但能操作原生的Redis数据结构,还为使用者提供了一系列具有分布式特性的常用工具类,实现了分布式锁。

Redis分布式锁和JUC的Lock方法相似。RLock接口继承了Lock接口

加锁解锁

java 复制代码
    @Test
    public void testLockDemo() {
        RLock disLock = client.getLock("DISLOCK");
        boolean isLock = false;
        try {
            disLock.lock(); //默认30s
//            isLock = disLock.tryLock(20000, 1500000, TimeUnit.MILLISECONDS);
            System.out.println(isLock);
            if (isLock) {

                //TODO if get lock success, do something;
                Thread.sleep(15000);
            }
        } catch (Exception e) {
        } finally {
            // 无论如何, 最后都要解锁
            disLock.unlock();
        }
    }

锁重入

java 复制代码
    @Test
    public void testLockDemo2() {
        RLock disLock = client.getLock("DISLOCK");
        boolean isLock = false;
        try {
            isLock = disLock.tryLock(2000, 1500000, TimeUnit.MILLISECONDS);
            isLock = disLock.tryLock(2000, 1500000, TimeUnit.MILLISECONDS);
            isLock = disLock.tryLock(2000, 1500000, TimeUnit.MILLISECONDS);

        } catch (Exception e) {
        } finally {
            // 无论如何, 最后都要解锁
            disLock.unlock();
            disLock.unlock();
            disLock.unlock();

        }
    }

锁的存储结构

锁的结构是Hash

key:锁的名字

字段: UUID+threadId

值:表示重入的次数

Redisson加锁原理

RedissonLock的tryLockInnerAsync是Redisson加锁的关键方法

加锁

1.判断有没有"DISLOCK"

2.如果没有,设置UUID:1=1

3.设置它的过期时间

锁重入

1.KEY和字段都存在,锁重入

2.执行命令incrby UUID:1 1

3.结果: DISLOCK: {UUID:1 2}

锁互斥

1.客户端2进入

2.判断有KEY,没有字段

3.返回过期时间

4.客户端2自旋等待

1.key不存在,加锁设置字段=1,过段时间

2.key、字段都存在,锁重入,字段值+1

3.key存在,字段不存在,抢锁失败

Redisson释放锁原理

RedissonLock的unlockInnerAsync是Redisson释放锁的关键方法

1.判断KEY是否存在

2.如果不存在,返回nil

3.如果存在,使用hincrby-1,减1

4.减完后,counter>0值仍大于0,则返回0

5.减完后,counter<=0,删除key

6.用publish广播锁释放消息

订阅channel源码

watch dog自动延期

watch dog:当加锁成功后,同时开启守护线程,默认有效期是30秒,每隔10秒就会给锁续期到30秒

watchDog只有在未显示指定加锁时间时才会生效

lockWatchdogTimeout:可以设置超时时间

分段锁

思想来源map/reduce,ConcurrentHashMap

相关推荐
IT大白鼠2 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz2 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙2 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除2 天前
redis--018
redis
两锭子2 天前
Redis基础
redis
烟沙九洲2 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索2 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞2 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路3 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog3 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis