查看原文
其他

springboot整合redis实现分布式锁

SpringForAll 2022-07-05
关注我,回复关键字“2022面经”
领取2022大厂Java后端面经

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>

如果您正在学习Spring Boot,那么推荐一个连载多年还在继续更新的免费教程:http://blog.didispace.com/spring-boot-learning-2x/

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);
 }

最近整理了一份最新的面试资料,里面收录了2021年各个大厂的面试题,打算跳槽的小伙伴不要错过,点击领取吧!

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";
}

如果您正在学习Spring Boot,那么推荐一个连载多年还在继续更新的免费教程:http://blog.didispace.com/spring-boot-learning-2x/

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的方式
  • 缓存数据时加上过期时间也足够解决大部分业务对于缓存的要求
  • 通过加锁保证并发读写,比如读写锁。如果业务不关心脏数据,可忽略此问题!

总结(最佳方案):

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

来源:https://blog.csdn.net/weixin_45865428/article

/details/122919158



END


关注后端面试那些事,回复【JAVA宝典】
获取最新免费Java资料大全
【往期推荐】

点击“阅读原文”领取2022大厂面经

↓↓↓

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存