分布式锁-Redis实现

1:Redis 分布式锁的原理

利用NX 的原子性,多线程并发时,只有一个线程可以设置成功

设置成功即获得锁,可以执行后续的业务处理

如果出现异常,过了锁的有效期,锁自动释放

释放锁用Redis 的delete 命令,然后释放锁的时候要校验锁的随机数,这个随机数相同才能释放,就是要证明Redis里面这个key 的值是你这个线程设置的,因为你这个线程在设置这个值得时候呢,是你的这个线程生成的这么一段随机数。删除的时候你要看程序设置的随机数和Redis 中的是不是相同的,如果相同就保证这个锁是你设置的。你才能够释放锁。确保你不会释放掉别人的锁。主要就是用作一个校验

释放锁采用 Lua 脚本,因为这个 delete 校验并没有提供值校验这么一个功能

获取锁的Redis命令:

set resource_name my_random_value NX PX 30000
  • resource_name  资源名称,可根据不同业务区分不同的锁
  • my_random_value  随机值,每个线程的随机值都不同,用于释放锁时的校验,要保证每一个线程的随机值都不相同。
  • NX:key 不存在时设置成功,key 存在则设置不成功。我们就是用这个命令实现分布式锁,主要就是用 NX 这个特性。因为SET NX是一个原子性的操作,我们都知道Redis 是一个单线程的,当你多线程并发的给这个key设置值之后,那么这个时候只有第一个线程会设置成功。因为并发请求过来时这些命令呢,在Redis  里边都变成了顺序的了,因为Redis 是单线程的所有你的并行变成了串行,这个就是排队了,只有第一个执行这个命令的才可以设置成功
  • PX:自动失效时间 ,当出现异常情况,锁可以过期失效。由于设置成功之后,后边的程序执行完成,你要把这个锁给释放了,释放成功以后其他线程才可以再次获得这个锁。如果没有设置失效时间,或者释放锁的过程出现异常,那你Redis 这条记录永远存在的,那么其他线程就永远无法获取到锁

为什么对值有这么高的要求?

主要是为了校验值一样了才可以删除锁

Redis分布式锁官方链接

2:手写Redis分布式锁

  1. 新建一个项目,导入依赖jar包
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>2.3.4.RELEASE</version>
            </dependency>
  2. 配置账号密码

    ​
    spring.redis.host=xx
    spring.redis.password=xx
  3. 整个代码实现过程

    package com.example.redislock.controller;
    
    
    import lombok.extern.slf4j.Slf4j;
    import org.redisson.Redisson;
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.redisson.config.Config;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.connection.RedisStringCommands;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.script.RedisScript;
    import org.springframework.data.redis.core.types.Expiration;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.Arrays;
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Author: qiuj
     * @Description:
     * @Date: 2020-10-01 15:45
     */
    @Slf4j
    @RestController
    public class LockController {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        
    
        @Transactional(rollbackFor = Exception.class)
        @RequestMapping("/redisLock")
        public String redisLock() {
            log.info("进入方法");
    
            String key = "lock";
            String value = UUID.randomUUID().toString();
    
            RedisCallback<Boolean> redisCallback = connection -> {
                RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
                Expiration expiration = Expiration.seconds(10);
                byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
                byte[] valueByte = redisTemplate.getValueSerializer().serialize(value);
                Boolean result = connection.set(keyByte, valueByte, expiration, setOption);
                return result;
            };
            Boolean lock = (Boolean) redisTemplate.execute(redisCallback);
            if (lock) {
                try {
                    log.info("获得了锁");
                    Thread.sleep(5000);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
    
                    String script = "            if  redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                            "                return  redis.call(\"del\",KEYS[1])\n" +
                            "            else\n" +
                            "                return 0\n" +
                            "            end";
                    RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
                    Boolean result = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(key), value);
                    log.info("释放锁的结果:" + result);
                }
            }
            log.info("方法执行完成");
            return "方法执行完成";
        }
    }
     
  4. 使用 redisTemplate.execute(RedisCallback<T> action)  方法
  5. RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
    需要实现  RedisCallback 回调接口, 设置选项为    SET_IF_ABSENT  也就是NX(如果没有这个key 则设置成功 ,如果已经存在则不成功)。           
  6. Expiration expiration = Expiration.seconds(10);
    设置锁失效时间为10秒,当持有锁超过10秒就会把锁释放。时间取决于你的业务代码块执行的时间进行相应的调整。
  7. byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
    byte[] valueByte = redisTemplate.getValueSerializer().serialize(value);                                                                           将key value 转为 Byte数组
  8. Boolean result = connection.set(keyByte, valueByte, expiration, setOption);                                   放入set 方法,返回结果true 就是设置key 成功,也就是获得到了锁。反之false 没有获得锁
            RedisCallback<Boolean> redisCallback = connection -> {
                RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
                Expiration expiration = Expiration.seconds(10);
                byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
                byte[] valueByte = redisTemplate.getValueSerializer().serialize(value);
                Boolean result = connection.set(keyByte, valueByte, expiration, setOption);
                return result;
            };
  9. 将我们实现的接口RedisCallback 放入 execute() 执行。获得到了锁执行业务代码当业务代码执行完需要释放锁。避免死锁
  10. 因为redis delete()并没有校验值这一方法,所以我们使用 Lua 脚本进行校验 value 匹配才允许删除 。并且使用脚本可以利用Redis 的原子性操作,取值、比较、删除是一个不可分割的操作。如果不使用脚本,3个步骤就分开了,会有并发的影响。例如我们在代码中获取值  if 判断 然后删除这并不是原子性操作
  11. if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
  12. 运行两个实例,8080在44 秒时获得了锁,8088在45秒时请求获得锁,但是已经被8080获得了,所以导致无法获取锁。所以在多应用,跨JVM 中实现了分布式锁,只有一个客户端能获取到锁

 8080

8088 

3:使用Redisson分布式锁

1:使用 api 实现

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.5</version>
        </dependency>
    @Transactional(rollbackFor = Exception.class)
    @RequestMapping("/redissonLock")
    public String redissonLock() {
        log.info("进入方法");
        Config config = new Config();
        config.useSingleServer().setAddress("redis://xxx:6379").setPassword("xxx");
        RedissonClient redissonClient = Redisson.create(config);
        RLock rLock = redissonClient.getLock("redissonLock");
        try {
            rLock.lock(10L, TimeUnit.SECONDS);
            log.info("获得了锁");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            log.info("释放锁");
        }
        log.info("方法执行完成");
        return "方法执行完成";
    }

2:使用 Spring Xml实现

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.5</version>
        </dependency>

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:redisson="http://redisson.org/schema/redisson"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://redisson.org/schema/redisson
       http://redisson.org/schema/redisson/redisson.xsd
">
    <!-- minimal requirement -->
    <redisson:client>
        <!-- defaults to 127.0.0.1:6379 -->
        <redisson:single-server address="redis://xxx:6379" password="xxx"/>
    </redisson:client>
    <!-- or -->
<!--    <redisson:client>-->
<!--        <redisson:single-server address="${redisAddress}"/>-->
<!--    </redisson:client>-->
</beans>
package com.example.redislock;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

@SpringBootApplication
@EnableScheduling
@ImportResource(locations = "classpath:redisson.xml")
public class RedisLockApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisLockApplication.class, args);
    }
}

 

    @Autowired
    private RedissonClient redissonClient;

    @Transactional(rollbackFor = Exception.class)
    @RequestMapping("/redissonSpringLock")
    public String redissonSpringLock() {
        log.info("进入方法");
        RLock rLock = redissonClient.getLock("redissonLock");
        try {
            rLock.lock(10L, TimeUnit.SECONDS);
            log.info("获得了锁");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            log.info("释放锁");
        }
        log.info("方法执行完成");
        return "方法执行完成";
    }

 

3:使用Spring Boot 实现

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

application.properties

spring.redis.host=xxx
spring.redis.password=xxx
    @Autowired
    private RedissonClient redissonClient;

    @Transactional(rollbackFor = Exception.class)
    @RequestMapping("/redissonSpringLock")
    public String redissonSpringLock() {
        log.info("进入方法");
        RLock rLock = redissonClient.getLock("redissonLock");
        try {
            rLock.lock(10L, TimeUnit.SECONDS);
            log.info("获得了锁");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            log.info("释放锁");
        }
        log.info("方法执行完成");
        return "方法执行完成";
    }

 4:实操-基于分布式锁解决定时任务重复执行问题

1:redis 的实现

需求:在集群环境下,每个应用的定时任务中,只能有一个应用执行这个方法。其他应用无法重复执行

  1. Application 启动类添加   @EnableScheduling 注解   ,用于开启 spring 定时任务
  2. 封装Lock
    package com.example.redislock.util;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.connection.RedisStringCommands;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.script.RedisScript;
    import org.springframework.data.redis.core.types.Expiration;
    
    import java.util.Arrays;
    import java.util.UUID;
    
    /**
     * @Author: qiuj
     * @Description:
     * @Date: 2020-10-02 17:29
     */
    @Slf4j
    public class RedisLock implements AutoCloseable{
    
    
        public RedisLock(String key,Integer timeOutTime,RedisTemplate redisTemplate) {
            this.redisTemplate = redisTemplate;
            this.key = key;
            this.timeOutTime = timeOutTime;
            this.value = UUID.randomUUID().toString();
        }
    
        private RedisTemplate redisTemplate;
    
        String value;
        String key;
        Integer timeOutTime;
    
        public Boolean lock () {
            RedisCallback<Boolean> redisCallback = connection -> {
                RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
                Expiration expiration = Expiration.seconds(timeOutTime);
                byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
                byte[] valueByte = redisTemplate.getValueSerializer().serialize(value);
                Boolean result = connection.set(keyByte, valueByte, expiration, setOption);
                return result;
            };
            Boolean lock = (Boolean) redisTemplate.execute(redisCallback);
            return lock;
        }
    
        @Override
        public void close() throws Exception {
    
            String script = "            if  redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                    "                return  redis.call(\"del\",KEYS[1])\n" +
                    "            else\n" +
                    "                return 0\n" +
                    "            end";
            RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
            Boolean result = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(key), value);
            log.info("释放锁的结果:" + result);
        }
    }
    

     

  3. cron 设置为每隔5秒钟执行一次方法
    package com.example.redislock.task;
    
    import com.example.redislock.util.RedisLock;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    /**
     * @Author: qiuj
     * @Description:
     * @Date: 2020-10-02 17:26
     */
    @Slf4j
    @Component
    public class SendTextMessage {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        /*
        需求:每隔5秒发送一次短信 。但是在多应用情况下不能重复
         */
        @Scheduled(cron = "*/5 * * * * ?")
        public void send() {
            try (RedisLock redisLock = new RedisLock("textMessage",10,redisTemplate)){
                if (redisLock.lock()) {
                    String message = "向13888888888发送一条短信";
                    log.info(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
  4. 开启两个应用 分别是 8080  8088
  5. 从  8080   从5秒-20秒   之间四次没有获得锁,8088则获取到锁。  8080从  20秒-35秒   之间成功获得锁,8088则没有获得锁

2:Redisson 的实现 

    /*
        需求:每隔5秒发送一次短信 。但是在多应用情况下不能重复
    */
    @Scheduled(cron = "*/5 * * * * ?")
    public void send() throws InterruptedException {
        RLock rLock = redissonClient.getLock("redissonLock");
        //  尝试加锁,最多等待0秒,上锁以后30秒后自动释放锁
        if (rLock.tryLock(0,30L, TimeUnit.SECONDS)) {
            try {
                String message = "向13888888888发送一条短信";
                log.info(message);
            } finally {
                rLock.unlock();
                log.info("释放锁");
            }
        }
    }

5:源码

源码

 

posted @ 2020-10-03 08:47  邱健  阅读(124)  评论(0编辑  收藏  举报