Redisson 分布式锁


前期配置


使用 Redisson 作为所有分布式锁、分布式对象等功能的框架

Redisson实现了JUC锁的接口

  • pom
<dependency>
	<groupId>org.redisson</groupId>
 	<artifactId>redisson</artifactId>
  	<version>3.12.5</version>
</dependency>
  • Config 程序化配置 Redisson
/***
 *  程序化配置 Redisson
 */
@Configuration
public class MyRedissonConfig {

    //所有对Redisson的使用都是通过RedissonClient对象
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson(){
        //1、创建配置
        Config config = new Config();
            //集群模式配置
//        config.useClusterServers().addNodeAddress("127.0.0.1:7004","127.0.0.1:7001";
            //单节点模式配置
        // 注意:Redis url should start with redis:// or rediss:// (for SSL connection)
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");

        //2、根据Config创建出RedissonClient实例
        RedissonClient redissonClient = Redisson.create(config);

        return redissonClient;
    }
}
  • 测试类 导入Autowired
@Autowired
RedissonClient redisson;

@Autowired
StringRedisTemplate redisTemplate;

1、lock锁 + 看门狗机制


  • 测试
    //测试简单服务 用于调优
    //redisson 测试锁机制
    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        //1、获取一把锁,只要锁名一样,就是同一把锁
        RLock lock = redisson.getLock("my-lock");
      
        //2、加锁
        lock.lock(); //阻塞式等待 默认的加锁时长是30s 锁自动过期被删掉

        /* 注意:这样写不会自动续期   【自动续期时间一定要大于业务时间】 */
//        lock.lock(10, TimeUnit.SECONDS); //设置10s自动解锁 推荐
      
        try {

            System.out.println("加锁成功,执行业务... " + Thread.currentThread().getId());
            Thread.sleep(30000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //3、解锁
            lock.unlock();
            System.out.println("释放锁--- " + Thread.currentThread().getId());
        }
        return "hello";
    }
}
  • 输出
加锁成功,执行业务... 94
释放锁--- 94
加锁成功,执行业务... 94
释放锁--- 94
加锁成功,执行业务... 96
释放锁--- 96

lock.lock(); 阻塞式等待 默认的加锁时长是30s 会执行看门狗

  1. redisson的自动续期,如果业务超长,运行期间自动续上30s,不用担心业务时间长,锁自动过期被删掉
  2. 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除
  3. 如果我们未指定锁的超时时间,底层就使用lockWatchdogTimeout = 30 * 1000;【看门狗默认时间】只要站锁成功,就会启动定时任务(重新给锁设置过期时间)新的时间就是看门狗的默认时间,每10秒,都会自动续期续成满时间

lock.lock(10, TimeUnit.SECONDS); 设置10s自动解锁 不会执行看门狗

  1. 注意:这样写不会自动续期自动续期时间一定要大于业务时间
  2. 如果我们指定锁的超时时间,就发送给redis执行脚本,进行占锁,默认时间就是我我们传递的时间
  3. 不过还是推荐指定过期时间。省掉了整个续期操作。我们可以将解锁时间和业务时间保持一致:例如此处案例,业务时间为30s,我们可以这样写:lock.lock(30, TimeUnit.SECONDS); 省掉续期操作,手动解锁

Redisson1


看门狗机制

  • lock.lock();

Redisson2


2、读锁 + 写锁


保证一定能读到最新的数据,修改期间,写锁是一个排他锁(互斥锁),读锁是一个共享锁写锁没释放读就必须等待

  • 读 + 读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,他们都会同时加锁成功
  • 写 + 读:读锁需要等待写锁释放
  • 写 + 写:阻塞方式
  • 读 + 写:写锁需要等待读锁释放

写锁:

    @ResponseBody
    @GetMapping("/write")
    public String writeValue() {
        //1、获取一把锁,只要锁名一样,就是同一把锁
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");

        String s = "";
        //1、改数据加写锁,读数据加读锁
        RLock wLock = lock.writeLock(); //拿到写锁
        try {
            wLock.lock(); //加锁
            System.out.println("写锁加锁成功......" + Thread.currentThread().getId());
            s = UUID.randomUUID().toString();
            Thread.sleep(30000); //睡眠30s
            redisTemplate.opsForValue().set("writeValue", s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            wLock.unlock(); //解锁
            System.out.println("写锁解锁..." + Thread.currentThread().getId());

        }
        return s;
    } 

读锁:

	@ResponseBody
    @GetMapping("/read")
    public String readValue() {
        //1、获取一把锁,只要锁名一样,就是同一把锁
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        String s = "";
        RLock rLock = lock.readLock(); //拿到读锁
        rLock.lock(); //加锁
        try {
            System.out.println("读加锁成功------" + Thread.currentThread().getId());
            s = redisTemplate.opsForValue().get("writeValue");
            Thread.sleep(30 000); //睡眠30s
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            rLock.unlock(); //解锁
            System.out.println("读锁解锁---" + Thread.currentThread().getId());

        }
        return s;
    }

  • JUC的写锁:new ReentrantReadWriteLock().writeLock()

  • JUC的读锁:new ReentrantReadWriteLock().readLock()


3、信号量


tryAcquire() : 当没有空闲时,直接返回false,不会等待
acquire() : 当没有空闲时,会等待信号量的释放


    //信号量测试
    /***
     *  车库停车 --> 拿到车位
     *  3个车位
     */
    @GetMapping("/park")
    @ResponseBody
    public String park() throws InterruptedException {
        RSemaphore park = redisson.getSemaphore("park");
        park.acquire(); //获取一个信号,获取一个值,占一个车位

//        boolean b = park.tryAcquire();
//        if (b){
//            //执行业务
//        }else {
//            return "error";
//        }

        return "ok => " + b ;

    }

    /***
     *  车开走 --> 释放车位
     */
    @GetMapping("/go")
    @ResponseBody
    public String go() throws InterruptedException {
        RSemaphore park = redisson.getSemaphore("park");
        park.release();
        return "ok"; //释放一个车位
    }


Redisson3


tryAcquire() --> 信号量也可以用作分布式限流

boolean b = park.tryAcquire();
if (b){
	//执行业务 
}else {
	return "error";
}
return "ok => " + b ;

Redisson4

JUC的信号量:

  • 车位数量:Semaphore semaphore = new Semaphore(5);
  • 释放车位:semaphore.release();
  • 占用车位:semaphore.acquire();

4、闭锁


//闭锁测试
    /***
     * 模拟场景:5个班级的人都走完了才可以锁大门
     */
    @GetMapping("/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {

        RCountDownLatch door = redisson.getCountDownLatch("door"); //拿到闭锁

        door.trySetCount(5); //等待的次数
        door.await(); //等待闭锁都完成

        return "放假了......";
    }

    @GetMapping("/gogogo/{id}")
    @ResponseBody
    public String gogogo(@PathVariable("id") Long id){
        RCountDownLatch door = redisson.getCountDownLatch("door"); //拿到闭锁
        door.countDown(); //计数-1
        return id + "班的人走了......";
    }

在这里插入图片描述

  • JUC的闭锁:CountDownLatch()

5、缓存一致性问题


双写模式

在这里插入图片描述

失效模式

Redisson7


解决方案

无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?

  • 1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
  • 2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
  • 3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
  • 4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);

总结:

  • 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
  • 我们不应该过度设计,增加系统的复杂性
  • 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

使用cache解决缓存一致性问题

在这里插入图片描述


案例: 使用Redisson的锁机制优化三级分类数据的查询

一定要注意锁的名字 --> 只要lock的名字一样,锁就一样

  • 锁的粒度,越细越快
  • 锁的粒度:具体缓存的是某个数据 实例:具体缓存的是11号商品 那么锁名应为 product-11-lock
    /***
     *  缓存里面的数据如何和数据库保持一致 --> 缓存数据一致性问题
     *  1)、双写模式
     *  2)、失效模式
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonDBWithRedissonLock() {

        //1、占分布式锁,去Redis占坑
        /* 一定要注意锁的名字 --> 只要lock的名字一样,锁就一样
         */
        RLock lock = redisson.getLock("catalogJson-lock");
        lock.lock(); //加锁
        Map<String, List<Catelog2Vo>> dataFromDb;
        try {
            dataFromDb = getDataFromDb();
        } finally {
            // 判断是否还在锁定状态
		    if (lock.isLocked()){
		        // 锁是否被当前线程持有
		        if (lock.isHeldByCurrentThread()){
		            // 解锁
		            lock.unlock();
		        }
		    }
        }
        return dataFromDb;
    }

注意 【可能会出现此异常 Error

IllegalMonitorstateException:attemot to unlock lock.not locked by current thread by node id:oda685f-81a…

  • 出现这个错误的原因:是在并发多的时候就可能会遇到这种错误,可能会被重新抢占
  • 不见得当前这个锁的状态还是在锁定,并且是本线程持有
  • 解决如下:
} finally {
    // 判断是否还在锁定状态
    if (lock.isLocked()){
        // 锁是否被当前线程持有
        if (lock.isHeldByCurrentThread()){
            // 解锁
            lock.unlock();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值