springboot整合redisson做分布式锁

文章目录

  • 一、简介
  • 二、引入redisson的依赖
  • 三、配置redisson
    • 3.1、编写配置文件
    • 3.2、官网的说明
  • 四、参照分布式锁和同步器
    • 4.1、官网
    • 4.2、简单概述
  • 五、案例演示
    • 5.0、统一说明
    • 5.1、可重入锁(Reentrant Lock)
    • 5.2、读写锁(ReadWriteLock)
    • 5.3、信号量(Semaphore)
    • 5.4、闭锁(CountDownLatch
  • 五、缓存里面的数据如何数据库保持一致


一、简介

redisson操作redis的一款框架,用来处理分布式锁等功能的。支持Java语言

redisson的github地址上面有详细的方法介绍==>官网地址

注明:如果是其他语言的分布式锁,可以去看redis官网 ==> 官网地址
springboot整合redisson做分布式锁_第1张图片

分布式情况下采用Redisson做分布式锁


如果是单体应用(就一个服务):我们完成可以采用本地锁(synchronized、lock)在进行同步。要区分好场景


本地锁 synchronized (this){} 同步代码块。只要是同一把锁,就能锁住需要这个锁的多线程
this==>springboot所有的组件在容器中都是单例的。所有注意这个this是唯一的


二、引入redisson的依赖

<!--引入redisson,作为所有分布式锁,分布式对象等功能的框架-->
<dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson</artifactId>
     <version>3.12.0</version>
</dependency>

下面的案例会用到操作redis的所有也将redis依赖引入

<!-
	这边是排除了lettuce,使用jedis做底层客户端
	但是还是推荐是lettuce做底层客户端,他使用netty通信,吞吐量更大(不排除lettuce,就不用引入jedis)
	lettuce有个bug。在压力测试的时候会产生堆外内存溢出,OutOfDirectMemoryError
		但是据说已经修复了
	jedis是稳定,但是太久没更新了
-->
<!--spring-boot-starter-data-redis-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
     <!--  排除掉lettuce客户端   -->
     <exclusions>
         <exclusion>
             <groupId>io.lettuce</groupId>
             <artifactId>lettuce-core</artifactId>
         </exclusion>
     </exclusions>
 </dependency>

 <!--引入jedis-->
 <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
 </dependency>

三、配置redisson

小编采用程序化配置、单节点模式

3.1、编写配置文件

MyRedissonConfig

package sqy.redissondome.config.redis;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

/**
 * Redisson配置类
 * @author suqinyi
 * @Date 2022/1/14
 */
@Configuration
public class MyRedissonConfig {

    /**
     * 官网配置说明:
     *      https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95
     *
     * Redisson来操作redis
     * 所有对Redisson的使用都是通过RedissonClient对象
     * 配置方法有
     *  第一种、 程序化配置方法
     *         config.useSingleServer().setAddress("redis://127.0.0.1:6379");
     *  第二种、 文件方式配置
     *         Config config = Config.fromYAML(new File("config-file.yaml"));
     *
     *  这是集群的程序化配置方法(方法1)
     * .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
        //创建配置
        Config config = new Config();
        //redis的连接地址,这个是小编虚拟机的redis
        //这边采用了【单节点模式】 - redis地址
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        //根据config创建出RedissonClient的实列
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }


//      //法一
//    @Bean
//    public RedissonClient getRedisson() {
//        Config config = new Config();
//        if(colony) {
//            config.useClusterServers()
//                    .setScanInterval(1000 * 2)
//                    .addNodeAddress(addressUrl)
//                    .setPassword(password)
//                    .setReconnectionTimeout(10000)
//                    .setRetryInterval(5000)
//                    .setTimeout(10000)
//                    .setConnectTimeout(10000);
//
//        }else {
//            config.useSingleServer()
//                    .setAddress(addressUrl).setPassword(password)
//                    .setReconnectionTimeout(10000)
//                    .setRetryInterval(5000)
//                    .setTimeout(10000)
//                    .setConnectTimeout(10000);
//        }
//
//        return Redisson.create(config);

//        //法二
//        config.useMasterSlaveServers()
//                //可以用"rediss://"来启用SSL连接
//                .setMasterAddress(addressUrl).setPassword(password)
//                .addSlaveAddress(addressUrlSlave)
//                .setReconnectionTimeout(10000)
//                .setRetryInterval(5000)
//                .setTimeout(10000)
//                .setConnectTimeout(10000);//(连接超时,单位:毫秒 默认值:3000);

        //  哨兵模式config.useSentinelServers().setMasterName("mymaster").setPassword("web2017").addSentinelAddress("***(哨兵IP):26379", "***(哨兵IP):26379", "***(哨兵IP):26380");

}

在配置下redis

spring:
  #配置redis
  redis:
    host: 192.168.56.10
    port: 6379

3.2、官网的说明

3.2.1、配置方法==>入口
springboot整合redisson做分布式锁_第2张图片
3.2.2、共有俩种配置方法

  1. 程序化配置方法
  2. 文件方式配置

3.2.3、单节点模式
因为小编没有把redis搭集群,所有采用的是单节点模式

独立节点 ==> 入口
在这里插入图片描述

代码如下(示例):

四、参照分布式锁和同步器

4.1、官网

地址==>入口
在这里插入图片描述

4.2、简单概述

  1. 分布式锁里面介绍了很多:可重入锁、公平锁、联锁、读写锁、 红锁
  2. 我们经常使用的是:可重入锁和读写锁
  3. 信号量(Semaphore):我们可以用来做限流
  4. 还有一个经常使用的:闭锁(CountDownLatch)也可以成为等待锁

可重入锁

基于Redis的Redisson分布式可重入锁RLock
Java对象实现了java.util.concurrent.locks.Lock接口。
同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

所以 ==> 以前Lock是怎么使用的、JUC是怎么使用的,redisson就怎么使用

五、案例演示

5.0、统一说明

记得注入RedissonClient 和StringRedisTemplate

 	@Autowired
    RedissonClient redisson;
    @Autowired
    StringRedisTemplate redisTemplate;

依赖包不要导入错了

import org.springframework.data.redis.core.StringRedisTemplate;
import org.redisson.api.*;

5.1、可重入锁(Reentrant Lock)

注:说明和源码的核心,小编写在注释里面了,可以自己搜索类进行看源码

  1. 看门狗机制,自动续期
  2. 自己传入过期时间,将不自动续期
  3. 推荐采用自己传入过期方式的模式,省掉了整个续期操作
 	@ResponseBody
    @GetMapping("/hello")
    public String hello() {
        // 获取一把锁,只要锁的名字一样,就是同一把锁
        RLock lock = redisson.getLock("my-lock");

        //方式一、加锁,默认的
        lock.lock();//阻塞式等待。默认加的锁都是30秒时间
        //方式二、可以指定时间,锁到期以后没有自动续期 ==> 所以自动解锁的时间一定要保证大于业务的执行时间
        //lock.lock(10,TimeUnit.SECONDS);//10s自动解锁

        //源代码:RedissonLock ==> 利用 while(ture)
      
        /**
         * 看门狗机制
         * 1、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心因为业务时间长,锁自动过期被删除
         * 2、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除
         *
         * 问题: lock.lock(10,TimeUnit.SECONDS) 在锁过期不会自动续期
         *      1、如果我们传递了锁的过期时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
         *      2、如果没有指定锁的超时时间,就使用30000L(LockWatchdogTimeout 看门狗的默认时间)
         *          只要占锁成功就会启动一个定时任务【就会重新给锁设置过期时间,新的时间就是看门狗的默认时间】,每隔10s都会自动续期,续期成30s
         *          this.internalLockLeaseTime(看门狗时间) / 3L   也就是10s续期一次
         */

        /**
         * 实战的使用推荐
         *    推荐使用:lock.lock(10,TimeUnit.SECONDS) 省掉了整个续期操作。手动解锁
         */
        try {
          	//业务
            System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
            //模拟业务耗时
            Thread.sleep(30000);
        } catch (Exception e) {

        } finally {
            /**
             * 假设出现闪断,解锁代码没有运行,redisson会不会出现死锁?
             * 不会出现死锁,锁的
             */
            //解锁
            System.out.println("释放锁..." + Thread.currentThread().getId());
            lock.unlock();
        }
        return "hello";
    }

测试:同时发送2个hello请求

效果:会等一个操作完,第二个才能执行
springboot整合redisson做分布式锁_第3张图片

5.2、读写锁(ReadWriteLock)

说明:准备2个方法,一个读取数据,一个写入数据

读写锁的好处小编之间总结在代码注释上面

	/**
     * 读写锁的好处:
     * 修改数据期间,写锁是一个排他锁(互斥锁\独享锁),读锁是一个共享锁
     * 写锁没释放,读锁就必须等着
     * 这样能保证读到最新数据
     * 读 + 读  相当于无锁(并发读,只会在redis中记录好,所有当前的读锁,他们都会同时加锁成功)
     * 写 + 读  等待写锁释放
     * 写 + 写  阻塞方式
     * 读 + 写  有读锁,写也需要等待
     * 

* 只要有写的存在,都必须等待 */ @ResponseBody @GetMapping("/write") public String writeValue() { RReadWriteLock lock = redisson.getReadWriteLock("rw-lock"); /** * 改数据加写锁,读数据加读锁 */ //加 写锁 RLock rLock = lock.writeLock(); rLock.lock(); String s = ""; try { System.out.println("写锁加锁成功。。。" + Thread.currentThread().getId()); s = UUID.randomUUID().toString(); Thread.sleep(30000); redisTemplate.opsForValue().set("writeValue", s); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("写锁释放成功。。。" + Thread.currentThread().getId()); //解锁 rLock.unlock(); } return s; } @ResponseBody @GetMapping("/read") public String readValue() { RReadWriteLock lock = redisson.getReadWriteLock("rw-lock"); //加 读锁 RLock rLock = lock.readLock(); rLock.lock(); String s = ""; try { System.out.println("读锁加锁成功。。。" + Thread.currentThread().getId()); s = redisTemplate.opsForValue().get("writeValue"); /** * 这边测试,先读在写 * 让读也延时30秒,看写锁是否需要等待 */ Thread.sleep(30000); } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("读锁释放成功。。。" + Thread.currentThread().getId()); rLock.unlock(); } return s; }

测试:我们可以模拟并发读写

  1. 测试1:先发一个写,在发2个读
  2. 测试2:先发一个读,在发2个写
  3. 测试3:发送3个读
  4. 测试4:发送2个写
  5. 这边小编就不把一个一个效果截图了。截图个,先1写后2读
  6. 说明:你们可以将配置复制一份,修改名称和端口,开2个服务。这样来模拟2个服务器,同时,进行读写操作。效果是一样的
    springboot整合redisson做分布式锁_第4张图片

5.3、信号量(Semaphore)

准备3个方法:一个在redis中存入数据,一个获取数据,一个释放数据

	/**
     * 信号量(Semaphore)
     * 都是阻塞方法
     * acquire() :会一直等,等到redis有数据,他就马上执行
     * tryAcquire():进行判断,如果为false,直接返回
     * 可以用于限流
     * 车库停车 ==> 一共3位置
     */

    //存入测试数据
    @GetMapping("/setPark")
    @ResponseBody
    public String setPark() {
        redisTemplate.opsForValue().set("park", "3");
        return "一共有3个空闲";
    }

    //获取一个数据(acquire)
    @GetMapping("/park")
    @ResponseBody
    public String park() throws InterruptedException {
        RSemaphore park = redisson.getSemaphore("park");
        park.acquire();//获取一个信息(获取一个值)占一个车位
        //执行业务
        String s = redisTemplate.opsForValue().get("park");
        return "redis剩余:" + s;

    }

    //获取一个数据(trypark)
    @GetMapping("/trypark")
    @ResponseBody
    public String trypark() throws InterruptedException {
        RSemaphore park = redisson.getSemaphore("park");
        boolean b = park.tryAcquire();
        if (b) {
            //执行业务
            String s = redisTemplate.opsForValue().get("park");
            return "redis剩余:" + s + "  " + b;
        } else {
            return "使用tryAcquire限流。封顶了你被直接返回";
        }
    }

    //释放一个数据
    @GetMapping("/go")
    @ResponseBody
    public String go() throws InterruptedException {
        RSemaphore park = redisson.getSemaphore("park");
        park.release();//释放一个信息(释放一个值)释放一个车位
        String s = redisTemplate.opsForValue().get("park");
        return "redis剩余:" + s;
    }

测试:

  1. 首先运行setPark方法
  2. 在运行park方法(或者trypark方法),运行3次以上,将存入redis的数据减少
  3. 在运行go方法,给redis里面存值
  4. 这种我们可以用来做限流操作
  5. 效果图小编就不贴了

5.4、闭锁(CountDownLatch

	/**
     * 放假,锁门
     * 5个班级全部走完,锁大门
     */

    @GetMapping("/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.trySetCount(5);//设置5个班
        door.await();//等待闭锁都完成
        return "放假了..";
    }

    //这个接口必须请求5次,闭锁才能释放
    @GetMapping("/gogogo/{id}")
    @ResponseBody
    public String gogogo(@PathVariable("id") Long id){
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.countDown();//计数减一。
        return id+"班的人走完了。。。";
    }

测试:

  1. 首先运行lockDoor方法。他就会一直转圈圈等待
  2. 在运行/gogogo/{id}方法。需要运行5次。这个闭锁才会释放

五、缓存里面的数据如何数据库保持一致

常见的2中模式:

  1. 双写模式:修改数据库数据的时候,同时修改缓存的数据
  2. 失效模式:修改数据库数据的时候,将缓存的数据直接删除,等待下次主动进行查询进行更新,存入缓存
  3. 这俩种模式:都可能因为网络问题,或者其他问题出现,在并发读写的情况下,产生脏数据(缓存和数据库最新数据不一致)
  4. 解决这个问题可以采用阿里的开源中间件Canal订阅来解决。但是更多的中间件加入会损耗一定的性能,对于频繁读写的数据,保持缓存数据一致性,我们都可以通过加锁来实现,但是要考虑,有没有必要,很多锁,也会导致系统(服务)变得缓慢
 系统的一致性解决方案:
     1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
     2、读写数据的时候,加上分布式的读写锁。
     经常写,经常读
 
     有没有必要存入缓存,加锁也会影响效率
     对经常读写的数据,要求实时性高的,直接去查数据库。

其他缓存当然还有:spring cache、guava…

推荐来看看官网的第三方框架整合(整合spring cache) ==> 入口
讲到分布式锁,就要有分布式事务。这边只能分篇章进行小结


完结

其他的比如说:集群、分片…之间参考官方文档搞就行了

面向官方文档编程

你可能感兴趣的:(SpringBoot和集成,redis,redisson,分布式锁,缓存)