redis分布式锁+redisson框架

目录

🧂1.锁的类型

🌭2.基于redis实现分布式

🥓[3. 基于redisson实现分布式锁](#3. 基于redisson实现分布式锁)


1.锁的类型

  • 1.本地锁:synchronize、lock等,锁在当前进程内,集群部署下依旧存在问题
  • 2.分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper, Mysql等都可以

2.基于redis实现分布式锁

1.加锁 setnx key value

  • setnx 的含义就是 SET if Not Exists,有两个参数 setnx(key, value),该方法是原子性操作
  • 如果 key 不存在,则设置当前 key 成功,返回 1;
  • 如果当前 key 已经存在,则设置当前 key 失败,返回 0

2.解锁 del(key)

得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用 del(key)

3.配置锁超时 expire(key,30)

客户端崩溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放

4.代码

正常的加锁逻辑,但存在问题

java 复制代码
  public void coupon(){
        String key="coupon_id";
        if (setnx(key,1)==1){
            expire(key,30, TimeUnit.MILLISECONDS)
            try{
                //业务
            }finally {
                del(key);
            }
        }else {
            //睡眠,然后自旋调用
            coupon();
        }
    }

5.存在的问题

  • 1.问题一
    • 多个命令之间不是原子性操作,如setnx和expire之间,如果setnx成功,则这个资源就是死锁但是expire失败,且宕机了,则这个资源就是死锁。
  • 2.解决
    • 使用原子命令:同时设置和配置过期时间 setnx / setex
    • redisTemplate.opsForValue().setIfAbsent(key, v: "lock", l: 30,TimeUnit.SECoNDS);
  • 1.问题二
    • 业务超时,存在其他线程勿删,key 30秒过期,假如线程A执行很慢超过30秒,则key就被释放了,其他线程B就得到了锁,这个时候线程A执行完成,而B还没执行完成,结果就是线程A删除了线程B加的锁
  • 2.解决
    • 可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁,那 value 应该是存当前线程的标识或者uuid
  • 3.但是,删除锁时也不是原子性操作。
    • 当线程A获取到正常值时,返回带代码中判断期间锁过期了,线程B刚好重新设置了新值,线程A那边有判断value是自己的标识,然后调用del方法,结果就是删除了新设置的线程B的值
    • 核心还是判断和删除命令 不是原子性操作导致

6.使用lua脚本

  • 问题: 加锁使用setnx setex可以保证原子性,那解锁使用判断和删除怎么保证原子性 ????
  • 多个命令的原子性:采用lua脚本+redis,由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败
    lua脚本
Lua 复制代码
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  • 使用redis+lua分布式锁,正常严谨的逻辑

redisTemplate.execute返回值不必须long 类型,但你必须确保 Redis 脚本返回的数据与你在 DefaultRedisScript 中指定的类型相匹配。

java 复制代码
public class Main {
    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * 分布式锁-redis
     */
    @Test
    public void test() {
        //根据业务,动态获取key
        String key = "coupon_id";
        //随机生成uuid,作为value值
        String uuid = CommonUtil.generateUUID();
        //设置key的同时,设置过期时间;获取锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, uuid, 30, TimeUnit.MILLISECONDS);
        //判断是否加锁成功
        if (lock) {
            //枷锁成功
            try {
                //执行业务
            } finally {
                //释放锁,使用lua脚本保持原子性
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                //执行lua脚本
                Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(key), uuid);
            }
        } else {
            //睡眠一定时间
            //加锁失败,自旋
            test();
        }
    }
}
  • 锁的过期时间,如何实现锁的自动过期 或者 避免业务执行时间过长,锁过期了?
  • 答:一般把锁的过期时间设置久一点,比如10分钟时间

3. 基于redisson实现分布式锁

1.添加依赖

XML 复制代码
            <!--分布式锁-->
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.12.0</version>
            </dependency>

2.添加redissonClient

如果报错的话,查看java版本是否正确

java 复制代码
@Configuration
@Data
public class RedissonConfig {

    //从配置文件获取redis的相关信息
    @Value("${spring.redis.host}")
    private String redisHost;
    @Value("${spring.redis.port}")
    private String redisPort;

    /**
     * 使用redisson作为分布式锁
     * @return
     */
    @Bean
    public RedissonClient redissonClient() {
        //1.创建配置对象
        Config config = new Config();
        //2.连接redis
        config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
        //3,创建redissonClient对象
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

3.实现分布式锁

myLock.lock()如果不指定过期时间,则默认30s后过期

java 复制代码
  @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/lock")
    public JsonData test() {
        //1.获取锁
        RLock myLock = redissonClient.getLock("my_lock");
        //2.手动加锁
        myLock.lock();
        try {
            //3.业务实现
            System.out.println("加锁成功~" + Thread.currentThread().getName());
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //4.手动解锁
            myLock.unlock();
            System.out.println("解锁成功~" + Thread.currentThread().getName());
        }
        return JsonData.buildSuccess();
    }

4.看门狗机制

Redis锁的过期时间小于业务的执行时间该如何续期?

  • 为了避免这种情况的发生, Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。
  • Redisson中客户端一旦加锁成功,就会启动一个watch dog看门狗。watch dog是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间。
  • 1.指定加锁时间

myLock.lock(10, TimeUnit.SECONDS)

  • 指定加锁时间后,默认10秒后过期,并且没有看门狗机制
  • 2.不指定加锁时间

myLock.lock(10, TimeUnit.SECONDS)

  • 默认30秒后过期,存在看门狗机制
  • 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔(【LockWatchingTimeOut看门狗默认时间】/3)这么长时间自动续期;
相关推荐
跟着珅聪学java32 分钟前
spring boot +Elment UI 上传文件教程
java·spring boot·后端·ui·elementui·vue
強云33 分钟前
界面架构- MVP(Qt)
qt·架构
我命由我1234537 分钟前
Spring Boot 自定义日志打印(日志级别、logback-spring.xml 文件、自定义日志打印解读)
java·开发语言·jvm·spring boot·spring·java-ee·logback
lilye6638 分钟前
程序化广告行业(55/89):DMP与DSP对接及数据统计原理剖析
java·服务器·前端
徐小黑ACG2 小时前
GO语言 使用protobuf
开发语言·后端·golang·protobuf
战族狼魂4 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
xyliiiiiL5 小时前
ZGC初步了解
java·jvm·算法
杉之6 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
morris1316 小时前
【redis】redis实现分布式锁
数据库·redis·缓存·分布式锁
hycccccch6 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq