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 {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是要执行的 Lua 脚本。这个脚本很简单,它只是返回一个包含四个元素的表(在 Lua 中,表是唯一的复合数据类型,类似于其他语言中的数组或字典)。这四个元素分别是脚本接收到的前两个 key 和前两个 argv。

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

key1 和 key2 是传递给 Lua 脚本的两个 key。在 Lua 脚本中,它们可以通过 KEYS[1] 和 KEYS[2] 来访问。

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

所以,当你执行这个命令时,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

相关推荐
BergerLee9 小时前
对不经常变动的数据集合添加Redis缓存
数据库·redis·缓存
huapiaoy9 小时前
Redis中数据类型的使用(hash和list)
redis·算法·哈希算法
【D'accumulation】10 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
Cikiss10 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
一休哥助手11 小时前
Redis 五种数据类型及底层数据结构详解
数据结构·数据库·redis
盒马盒马12 小时前
Redis:zset类型
数据库·redis
Jay_fearless14 小时前
Redis SpringBoot项目学习
spring boot·redis
Wang's Blog14 小时前
Redis: 集群环境搭建,集群状态检查,分析主从日志,查看集群信息
数据库·redis
wclass-zhengge20 小时前
Redis篇(最佳实践)(持续更新迭代)
redis·缓存·bootstrap
Dylanioucn20 小时前
【分布式微服务云原生】探索Redis:数据结构的艺术与科学
数据结构·redis·分布式·缓存·中间件