Spring Boot 整合——Redis分布式锁的简单实现

共享的数据

当我们的系统在一起的时候,假如去操作共享的数据的时候,我们可以加锁来保证数据的安全。但是当我们系统被拆分成多个服务,或者不同的系统去共享一部分数据,而此数据可以被多个系统修改的时候,比如多个系统去修改一个订单的信息。为了保证数据的可靠,我们需要在分布式或者多系统之间找出一种锁的实现方式。

而在分布式系统中实现锁的途径有多种。这里我们先讲Reids的方式。

首先要声明的本例子使用的版本信息

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.8.RELEASE</version>
        <relativePath/> 
    </parent>

另外本例子只是一个很简单的实现,并没有实现锁的重入。

锁的基本要求

我们要实现一个锁,一般来说要保证以下要求;

  1. 互斥,同一时间我们需要保证只有一个(线程)客户端能够获取到锁。
  2. 避免死锁,我们需要尽量避免出现死锁,当持有锁的客户端崩溃的时候,需要有其他方式使锁被释放。
  3. 客户端只能解锁自己的锁,不能去解锁别人的锁。

上面的要求就需要我们保证代码在设置并且判断是否有值的时候需要原子操作,以及锁需要有个时间、保证锁最终可以成功释放掉。

分布式锁的运作

假设两个系统都是订单处理系统,现在都去订单系统中拉取订单进行处理。这个时候可能存在APP1和APP2都拉取一个订单进行处理。这样就可能出现重复处理。

APP1
订单
APP2

那么我们现在在订单之前加一个获取锁的操作

APP1
APP2
订单

此时我们APP1 优先请求订单1的锁,此时APP2再去申请订单1的锁会被拒绝。这样就避免重复操作。

拿到订单1的锁
申请订单1锁失败,转去操作别的订单
APP1
APP2
订单

ps.关于第三条,存在一个极端情况

APP1 AAP2 AAP3 成功申请锁 超时、锁被释放 成功申请锁 APP1释放锁,此时不去验证锁的value导致释放掉AAP2的锁 此时APP3应该申请不到锁的,但实际上却因为APP1把APP2的锁释放掉导致申请成功 APP1 AAP2 AAP3

实现锁的方式

lua脚本

redisTemplate.opsForValue().setIfAbsent方法虽然内部使用了connection.setNX的方法,虽然保证设置参数的原子性,但是在之前版本中设置超时时间的时候却需要额外操作。为了保证原子化的操作,所以我们将判断和参数设置写到lua脚本中,通过调用过程执行来保证原子操作。

编写lua脚本

    /**
     * 拿锁的脚本
     */
    private static final String LUA_SCRIPT_LOCK = "return sample.redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) ";

    /**
     * 拿锁的脚本
     */
    private static RedisScript<String> scriptLock = new DefaultRedisScript<>(LUA_SCRIPT_LOCK, String.class);

    /**
     * 释放锁锁的脚本
     */
    public static final String LUA_SCRIPT_UN_LOCK = "if " +
                                                "(sample.redis.call('GET', KEYS[1]) == ARGV[1] )" +
                                          "then " +
                                                "return sample.redis.call('DEL', KEYS[1]) " +
                                          "else " +
                                                "return 0 " +
                                          "end";

    /**
     * 释放锁的脚本
     */
    private static RedisScript<Long> scriptLock2 = new DefaultRedisScript<>(LUA_SCRIPT_UN_LOCK, Long.class);

获得锁

    public RedisLock getLock(String key,long timeOut,long tryLockTimeout) {
        long timestamp = System.currentTimeMillis();
        try {
            key = getKey(key);
            UUID uuid = UUID.randomUUID();
            while ((System.currentTimeMillis() - timestamp) < tryLockTimeout) {
                // 参数分别对应了脚本、key序列化工具、value序列化工具,后面参数对应scriptLock字符串中的三个变量值,
                // KEYS[1],ARGV[1],ARGV[2],含义为锁的key,key对应的value,以及key 的存在时间(单位毫秒)
                String result = (String) redisTemplate.execute(scriptLock,
                        redisTemplate.getStringSerializer(),
                        redisTemplate.getStringSerializer(),
                        Collections.singletonList(key),
                        uuid.toString(),
                        String.valueOf(timeOut));
                if (result != null && result.equals("OK")) {
                    return new RedisLock(key, uuid.toString());
                } else {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error(JSON.toJSONString(e.getStackTrace()));
        }
        return null;
    }

释放锁


    /**
     * 释放锁
     * @param lock
     */
    public void releaseLock(RedisLock lock) {
        Object execute = redisTemplate.execute(scriptLock2,
                redisTemplate.getStringSerializer(),
                redisTemplate.getStringSerializer(),
                Collections.singletonList(lock.getKey()),
                lock.getValue()
        );
        // 当返回0的时候可能因为超时而锁已经过期
        if (new Integer("1").equals(execute)) {
            log.info("释放锁");
        }
    }

锁对象

@Data
public class RedisLock {

    /**
     * 锁的key
     */
    private String key;
    /**
     * 锁的值
     */
    private String value;

    public RedisLock(String key, String value) {
        this.key = key;
        this.value = value;
    }
}

测试

因为我们只是一个很简单的实现,没有实现锁重入,所以只是简单的一个线程测试了,锁的效果

多个应用请求一个锁,此时第二个请求无法获得锁

    public void testLock() {
        redisTemplate.delete("redisLock");
        redisTemplate.delete("redisLock2");

        RedisLock redisLock1 = manager.getLock("redisLock", 10000L, 5000L);
        if (redisLock1 == null) {
            log.info("未获得锁");
        } else {
            log.info("获得锁");
        }
        Assert.assertNotNull(redisLock1);
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 因为非重入锁,所以此时再获取锁失败
        RedisLock redisLock2 = manager.getLock("redisLock", 10000L, 5000L);
        Assert.assertNull(redisLock2);
        if (redisLock2 == null) {
            log.info("未获得锁");
        } else {
            log.info("获得锁");
        }
    }

获得超时锁

此时因为上一个请求锁到期,后面的请求也可以获得锁。

    @Test
    public void testLock2() {

        redisTemplate.delete("redisLock");
        redisTemplate.delete("redisLock2");

        RedisLock redisLock3 = manager.getLock("redisLock2", 10000L, 5000L);
        if (redisLock3 == null) {
            log.info("未获得锁");
        } else {
            log.info("获得锁");
        }
        Assert.assertNotNull(redisLock3);
        try {
            Thread.sleep(15000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 因为非重入锁,所以此时再获取锁失败
        RedisLock redisLock4 = manager.getLock("redisLock2", 10000L, 5000L);
        Assert.assertNotNull(redisLock4);
        if (redisLock4 == null) {
            log.info("未获得锁");
        } else {
            log.info("获得锁");
        }
    }

正常的释放和获得锁

    @Test
    public void testLock3() {

        redisTemplate.delete("redisLock");
        redisTemplate.delete("redisLock2");
        redisTemplate.delete("redisLock3");

        RedisLock redisLock3 = manager.getLock("redisLock3", 10000L, 5000L);
        if (redisLock3 == null) {
            log.info("未获得锁");
        } else {
            log.info("获得锁");
        }
        Assert.assertNotNull(redisLock3);
        manager.releaseLock(redisLock3);
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 因为非重入锁,所以此时再获取锁失败
        RedisLock redisLock4 = manager.getLock("redisLock2", 10000L, 5000L);
        Assert.assertNotNull(redisLock4);
        if (redisLock4 == null) {
            log.info("未获得锁");
        } else {
            log.info("获得锁");
        }
    }


使用最新的版本

在2.1版本之后spring-data-redis丰富了setIfAbsent方法。

[外链图片转存失败(img-JzsGKNyq-1565530962149)(DEDE24A8084749A19F83DF73021C0BB9)]

要使用这个方法,之前我尝试将spring-boot-starter-data-redis修改为最新的2.1.7后来发现实际引入的spring-data-redis还是2.0.13所以我只能修改我们的依赖。

    <dependencies>
        <dependency>
            <artifactId>base-core</artifactId>
            <groupId>daifyutils</groupId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.1.6.RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
            <version>5.1.6.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.2</version>
        </dependency>
    </dependencies>
</project>

加锁

我们可以使用这个方法完成加锁

    public RedisLock getLock(String key,long timeOut,long tryLockTimeout) {
        long timestamp = System.currentTimeMillis();
        try {
            key = getKey(key);
            UUID uuid = UUID.randomUUID();
            while ((System.currentTimeMillis() - timestamp) < tryLockTimeout) {
                Boolean aBoolean =
                        redisTemplate.opsForValue().setIfAbsent(key, uuid.toString(), timeOut, TimeUnit.MILLISECONDS);
                if (aBoolean) {
                    return new RedisLock(key, uuid.toString());
                } else {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error(JSON.toJSONString(e.getStackTrace()));
        }
        return null;
    }

当然这种方法目前我保留意见,因为需要我们额外修改依赖版本,会不会出现未知的问题,暂时没法确定。

一个小坑

之前写代码的时候出现过Redis exception; nested exception is io.lettuce.core.RedisException: java.lang.IllegalStateException的错误。

org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is io.lettuce.core.RedisException: java.lang.IllegalStateException

    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:74)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)
    at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
    at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
    at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:257)
    at org.springframework.data.redis.connection.lettuce.LettuceScriptingCommands.convertLettuceAccessException(LettuceScriptingCommands.java:236)
    at org.springframework.data.redis.connection.lettuce.LettuceScriptingCommands.evalSha(LettuceScriptingCommands.java:195)
    at org.springframework.data.redis.connection.DefaultedRedisConnection.evalSha(DefaultedRedisConnection.java:1240)

当时是因为我设置了Integer.class的返回类型

    private static RedisScript<Integer> scriptLock2 = new DefaultRedisScript<>(LUA_SCRIPT_UN_LOCK,Integer.class);

后来发现支持的返回类型判断,并不存在Integer

        if (javaType == null) {
            return ReturnType.STATUS;
        }
        if (javaType.isAssignableFrom(List.class)) {
            return ReturnType.MULTI;
        }
        if (javaType.isAssignableFrom(Boolean.class)) {
            return ReturnType.BOOLEAN;
        }
        if (javaType.isAssignableFrom(Long.class)) {
            return ReturnType.INTEGER;
        }
        return ReturnType.VALUE;

本篇文章涉及的源码下载地址:https://gitee.com/daifyutils/springboot-samples

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Boot实现Redis分布式锁可以通过以下步骤: 1. 添Redis依赖:在`pom.xml`文件中添Redis的依赖,例如: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` 2. 配置Redis连接信息:在`application.properties`或`application.yml`文件中配置Redis连接信息,例如: ```properties spring.redis.host=127.0.0.1 spring.redis.port=6379 ``` 3. 创建Redis分布式锁实现类:创建一个实现分布式锁接口的类,例如`RedisDistributedLock`,在该类中注入`StringRedisTemplate`用于操作Redis。 4. 实现锁方法:在`RedisDistributedLock`类中实现锁方法,可以使用Redis的`setnx`命令来进行锁操作,例如: ```java public boolean lock(String key, String value, long expireTime) { Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS); return success != null && success; } ``` 5. 实现释放锁方法:在`RedisDistributedLock`类中实现释放锁方法,使用Redis的`del`命令来删除锁对应的键,例如: ```java public boolean unlock(String key) { return redisTemplate.delete(key); } ``` 6. 在业务代码中使用分布式锁:在需要锁的代码块前后调用锁和释放锁方法,例如: ```java @Autowired private RedisDistributedLock redisDistributedLock; public void doSomethingWithLock() { String lockKey = "my-lock"; String lockValue = UUID.randomUUID().toString(); long expireTime = 10000; // 过期时间,单位为毫秒 try { boolean locked = redisDistributedLock.lock(lockKey, lockValue, expireTime); if (locked) { // 执行业务逻辑 } else { // 获取锁失败,可以进行重试或处理其他逻辑 } } finally { redisDistributedLock.unlock(lockKey); } } ``` 通过以上步骤,就可以在Spring Boot实现Redis分布式锁。注意在使用分布式锁时需要考虑锁的粒度和超时处理等问题,以确保分布式锁的正确使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值