springboot整合redis实现分布式锁

25 篇文章 0 订阅
18 篇文章 2 订阅

redis常见问题

  • 缓存穿透:程序中没有缓存null值;当大量请求获取一个不存在的数据时,由于缓存中没有缓存到null值,大量请求直接访问数据库,数据库压力陡增,从而出现穿透问题!

    • 解决方案:将查询结果为null的值缓存到redis中
  • 缓存雪崩:大量缓存同一个时间内失效;

    • 解决方案:在设置数据有效时间时,增加一个随机数
  • 缓存击穿:大量请求同时访问同一个正好过期的缓存数据

    • 解决方案:添加分布式锁

一、原生方式

参考文档:https://github.com/redisson/redisson/wiki/Table-of-Content

1、导入依赖

<!--原生redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.11.0</version>
</dependency>

<!--操作redisTemplate-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、创建配置

 /**
  * 所有对redisson的使用都是通过RedissonClient对象
  * @return
  * @throws IOException
  */
 @Bean(destroyMethod="shutdown")
 public RedissonClient redisson() throws IOException {
     //创建配置
     Config config = new Config();
     //可以用"rediss://"来启用SSL连接,useSingleServer表示单例模式
     config.useSingleServer().setAddress("redis://127.0.0.1:6379");
     //根据config创建出RedissonClient实例
     return Redisson.create(config);
 }

3、测试RedissonClient 对象是否创建

@Autowired
RedissonClient redisson;

@Test
public void test(){
    System.out.println(redisson);
}

出现如下结果表示测试通过
在这里插入图片描述

4、测试分布式锁

注意:为避免出现死锁,所有关于锁的程序设计都尽量设计为可重入锁(Reentrant Lock)

4.1、解决死锁

@Autowired
RedissonClient redisson;

@ResponseBody
@RequestMapping("/hello")
public String hello(){
    //1、获取一把锁,只要锁的名字一样,那就是同一把锁
    RLock lock = redisson.getLock("my-lock");
    //2、加锁,默认加的锁都是30s时间
    lock.lock(); //阻塞式等待
    //1)、锁的自动续期;如果业务超长,运行期间自动给锁续上新的30s;不用担心业务时间长,锁自动过期被删掉
    //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除

    /**
     * lock.lock(10, TimeUnit.SECONDS); //10秒自动解锁;解锁时间一定要大于业务操作时间
     * 问题:如果指定解锁时间,在锁时间到了以后,不会自动续期
     * 1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
     * 2、如果我们未指定超时时间,就使用30*1000【lockWatchdogTimeout看门狗默认的时间】,只要占锁成功,就会
     *      启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒就会自动续期,续成30s
     */
    //最佳实践
    //lock.lock(30, TimeUnit.SECONDS); 指定时间,并手动解锁
    try {
        System.out.println("加锁成功,执行业务。。。"+Thread.currentThread().getId());
        Thread.sleep(30000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("释放锁。。。"+Thread.currentThread().getId());
        lock.unlock();
    }
    return "hello";
}

4.2、读写锁

 @Autowired
 RedissonClient redisson;
 
 @Autowired
 RedisTemplate redisTemplate;

//写锁保证一定能读到最新数据,修改期间,写锁是一个排他锁(互诉锁,独享锁)。读锁是一个共享锁
//写锁没释放,读就必须等待
//写 + 读 (写的时候进行读操作):等待写锁释放
//写 + 写 (写的时候进行写操作):阻塞方式
//读 + 写 (读的时候进行写操作):等待读锁释放
//读 + 读 :相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功。
//总结:只要有写的存在,就必须等待前面的锁释放。
@ResponseBody
@RequestMapping("/write")
public String writeLock(){
    RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
    String s = "";
    RLock rLock = lock.writeLock();
    try {
        // 改数据加写锁,读数据加读锁

        rLock.lock();
        s = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set("writerValue",s);
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }
    return s;
}

@ResponseBody
@RequestMapping("/read")
public String readLock(){
    RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
    RLock rLock = lock.readLock();
    String s = "";
    try {
        //加读锁
        rLock.lock();
        s = redisTemplate.opsForValue().get("writerValue").toString();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }
    return s;
}

4.3、闭锁

/**
 * 下班了,关门回家
 * 1、部门没人了
 * 2、5个部门全部走完,锁门回家
 * @return
 * @throws InterruptedException
 */
@ResponseBody
@RequestMapping("/lockDoor")
public String lockDoor() throws InterruptedException {
    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.trySetCount(5);
    door.await(); // 等待闭锁都完成
    return "下班了。。。。";
}

@ResponseBody
@RequestMapping("/gogogo/{id}")
public String gogogo(@PathVariable("id") String id){
    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.countDown(); //计数减1
    return id + "部门下班了";
}

4.4、信号量

注:信号量也可以用作分布式限流

/**
 * 车库停车(信号量)
 * 3车位
 * 信号量也可以用作分布式限流
 * @return
 */
@ResponseBody
@RequestMapping("/park")
public String park() throws InterruptedException {
    RSemaphore park = redisson.getSemaphore("park");
    //park.acquire(); //获取一个信号,获取一个值,占一个车位(阻塞方式)
    boolean b = park.tryAcquire();//直接运行之后的代码,非阻塞
    if (b){
        //执行业务
    }
    return "ok";
}

@ResponseBody
@RequestMapping("/gogo")
public String gogo(){
    RSemaphore park = redisson.getSemaphore("park");
    park.release(); //释放一个车位,车开走了
    return "ok";
}

4.5、解决缓存一致性问题

常见的两种方式:

  • 双写模式:修改数据完成后,直接修改缓存中的数据
  • 失效模式:修改数据完成后,删掉缓存中的数据,等待下次主动查询进行更新
    在这里插入图片描述
    解决双写模式出现脏数据的问题:
  • 给并发写操作加写锁
  • 如果系统允许数据出现短暂的不一致,可忽略!等待数据过期自动删除,下次主动查询再缓存!

在这里插入图片描述
解决失效模式出现脏数据的问题:

  • 加写锁

由此看到,无论是哪种模式,都会导致缓存不一致的问题,怎么办?

  • 如果是用户比较稳定的数据(订单数据,用户数据),并发几率小,不用考虑缓存不一致问题,缓存时直接加上失效时间,下次查询时自动更新缓存!
  • 如果是菜单,商品介绍等基础数据,可以使用canal订阅binlog的方式
  • 缓存数据时加上过期时间也足够解决大部分业务对于缓存的要求
  • 通过加锁保证并发读写,比如读写锁。如果业务不关心脏数据,可忽略此问题!

总结(最佳方案)

  • 实时性、一致性高的数据(读多写多),直接走查询数据库。
  • 缓存数据时,加上过期时间,保证每天拿到的数据是当前最新数据。
  • 读写数据的时候,加上分布式的读写锁(写操作频繁的除外)。
  • 不应过度设计,增加系统难度。
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值