美文网首页
Redission 分布式锁框架

Redission 分布式锁框架

作者: 鄙人_阿K | 来源:发表于2021-12-19 17:51 被阅读0次

    Redis6总纲

    https://www.jianshu.com/p/901dc5a0c683

    文档

    https://github.com/redisson/redisson/wiki/Table-of-Content

    JUC

    学过juc可以和 redission 无缝衔接
    https://www.jianshu.com/p/aa02b491afba

    目录

    1、构建项目
    2、看门狗原理如何解决死锁
    3、读写锁
    4、信号量
    5、闭锁
    6、分布式锁解决缓存一致性问题

    概述

    Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

    一、项目搭建

    1、坐标引入

    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.12.0</version>
    </dependency>
    

    2、配置

    单节点配置

    package com.kk.springbootredis.config;
    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;
    @Configuration
    public class RedissonConfig {
        /*
         * @Description:    所有对 Redisson的使用都是通过 RedissonClient对象
         * @Author:         阿K
         * @CreateDate:     2021/12/16 21:34
         * @Param:          []
         * @Return:         org.redisson.api.RedissonClient
        **/
        @Bean(destroyMethod = "shutdown")
        RedissonClient redisson() throws IOException {
    
            // 1、创建配置
            Config config = new Config ( );
            config.useSingleServer ().setAddress ("redis://106.52.23.202:6379");
        // 集群模式
        // useClusterServers ( )
        //.addNodeAddress ("redis://127.0.0.1:7004", "redis://127.0.0.1:7001");
        // 密码设置
        //.setPassword ("abc123456");
    
            // 2、根据 Config 实例创建出 RedissonClient 对象
            return Redisson.create (config);
        }
    }
    
    

    以下两种为官网提供的参考:

    1、单节点
    // 默认连接地址 127.0.0.1:6379
    RedissonClient redisson = Redisson.create();
    
    Config config = new Config();
    config.useSingleServer().setAddress("myredisserver:6379");
    RedissonClient redisson = Redisson.create(config);
    
    2、集群
    Config config = new Config();
    config.useClusterServers()
        .setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
        //可以用"rediss://"来启用SSL连接
        .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
        .addNodeAddress("redis://127.0.0.1:7002");
    
    RedissonClient redisson = Redisson.create(config);
    

    二、第一个Demo

    官网:


    image.png

    1、第一个 demo

    @Controller
    public class TestRedissonClient {
        @Autowired
        RedissonClient redisson;
    
        @ResponseBody
        @GetMapping("/hello")
        public String hello(){
            // 1、获取一把锁,只要锁的名字一样,既是同一把锁
            RLock lock = redisson.getLock ("my-lock");
    
            // 2、加锁
            lock.lock ();// 阻塞式等待
    
            try {
                System.out.println ("加锁成功,执行业务..."+Thread.currentThread ().getId () );
                // 模拟超长等待
                Thread.sleep (20000);
            } catch (Exception e) {
                e.printStackTrace ( );
            }finally {
                // 3、解锁
                System.out.println ("释放锁..."+Thread.currentThread ().getId () );
                lock.unlock ();
            }
            return "hello";
        }
    }
    

    发送两个请求


    image.png

    打印结果

    加锁成功,执行业务...80
    释放锁...80
    加锁成功,执行业务...81
    释放锁...81
    

    二、看门狗原理

    1、模拟两个服务,A服务闪掉还没释放锁的情况

    A服务先运行,在运行B服务,还没释放A的是否,A就挂了,锁也还没释放,会不会死锁呢??

    image.png

    启动两台,8080,8081(闪断--手动关闭)


    image.png

    发现80没有因为81没有释放而导致死锁,因为底层有看门狗机制。

    追溯源码:自旋,不断尝试获取锁


    image.png

    2、默认 lock() 小结

    (1)默认指定锁时间为30s(看门狗时间)
    (2)锁的自动续期:若是业务超长,运行期间自动给锁上新的 30s,不用担心业务时间过长,锁就自动过期
    (3)加锁的业务只要运行完成,就不会给当前锁续期,及时不手动解锁,锁默认在30s 后自动删除。

    3、指定时间 lock() --- 【推荐写法】

    // 10s自动解锁,指定时间一定要大于业务时间(不然会报错,没把握就不要用)
    lock.lock (10, TimeUnit.SECONDS);
    

    4、指定时间 lock() 小结

    问题:在锁到的时候,不会自动续期。
    (1)如果我们传递了锁的超时时间,就发送给 redis执行脚本,进行占锁,默认的超时时间既我们指的时间
    (2)若是未指定锁的超时时间,就使用 30*1000【LockWatchdogTimeout看门狗的默认时间】
    (3)只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔 10 s都会自动再次续到30s, internallockLeaseTime【看门狗时间/3s】

    三、读写锁

    1、代码

        @Autowired
        RedissonClient redisson;
        @Autowired
        RedisTemplate redisTemplate;
    
    
        // 并发写测试
        @GetMapping("write")
        @ResponseBody
        public String writeValue(){
            RReadWriteLock myLock = redisson.getReadWriteLock ("my_lock");
            RLock rLock = myLock.writeLock ( );
            rLock.lock ();
            String s = "";
            try {
                System.out.println ("写锁加锁成功..."+Thread.currentThread ().getId () );
                s= UUID.randomUUID ().toString ();
                TimeUnit.SECONDS.sleep (30);
                redisTemplate.opsForValue ().set ("writeValue",s);
            } catch (Exception e) {
                e.printStackTrace ( );
            }
            finally {
                System.out.println ("写锁解锁成功..."+Thread.currentThread ().getId () );
                rLock.unlock ();
            }
            return s;
        }
        // 并发读测试
        @GetMapping("read")
        @ResponseBody
        public String readValue(){
            RReadWriteLock myLock = redisson.getReadWriteLock ("my_lock");
            RLock rLock = myLock.readLock ( );
            rLock.lock ();
            String s = "";
            try {
                System.out.println ("读锁加锁成功..."+Thread.currentThread ().getId () );
                s = (String) redisTemplate.opsForValue ().get ("writeValue");
            } catch (Exception e) {
                e.printStackTrace ( );
            }finally {
                System.out.println ("读锁解锁成功..."+Thread.currentThread ().getId () );
                rLock.unlock ();
            }
            return s;
        }
    
    

    2、结论
    保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁)。读锁是一个共享锁
    (1)读+读:相当于无锁,并发读情况下,只会在 redis 中记录好,所有当前的读锁,他们都会加锁成功。
    (2)写+读:等待写锁释放
    (3)写+写:阻塞方式
    (4)读+写:有读锁,写也需要等待

    只要有写的存在,都必须等待

    四、信号量

        /**
         * 车库停车:
         * 信号量也可以做分布式限流
         * @return
         */
        @GetMapping("/park")
        public String park()throws Exception{
            RSemaphore park = redisson.getSemaphore ("park");
    
            // 获取一个信号,获取一个值,占一个车位
            //park.acquire ();
            //park.acquire (23);// 占用23个
    
            // 如果有数量则占用,没有则失败
            boolean b = park.tryAcquire ( );
            if (b){
                // 执行业务
            }else {
                return "没有可以占用的,失败";
            }
            return "占用成功,当前剩余车可占用量:"+park.availablePermits ();
        }
    
    
    
        @GetMapping("/go")
        public String go(){
            RSemaphore park = redisson.getSemaphore ("park");
            //park.release ();// 开发一个车位
            park.release (30);// 开发 30个车位
            return "增加车位成功";
        }
    

    五、闭锁

        /**
         * 模拟 5 个班级人走完,学校放假(闭校)
         * @return
         * @throws Exception
         */
        @GetMapping("lockDoor")
        public String lockDoor()throws Exception{
            RCountDownLatch door = redisson.getCountDownLatch ("door");
            door.trySetCount (5);
            door.await ();// 等待闭锁都完成
            return "放假了";
        }
    
        @GetMapping("/gogo/{id}")
        public String gogo(@PathVariable("id")String id){
            RCountDownLatch door = redisson.getCountDownLatch ("door");
            door.countDown ();// 计数-1
            long count = door.getCount ( );
            return id+"班走完,剩余:"+count;
        }
    

    六、分布式锁解决缓存一致性问题

    1、redisson底层的每个锁 api调用都是使用 lua脚本(保证原子性)

    image.png

    2、锁的粒度:越细越快,越粗越慢

    就是锁的名字不要尽量不要相同,比如一个接口的写锁大并发(100W),一个接口写锁小并发(1000),他们若是同一把锁,1000的并发可能要等100W的并发执行完成,所以设置锁名字要避免相同。

    3、缓存一致性问题

    缓存和数据库如何保持一致性
    (1)双写模式
    (2)失效模式

    4、双写模式

    既数据库改完,再改缓存


    image.png

    双写模式和失效模式在大并发下都会有问题:
    问题:会有脏数据
    方案一:加锁
    方案二:若是允许延迟(今天更新的数据明天展示,或者几分钟几小时延迟),设置过期时间 (建议)

    5、失效模式

    将数据库改完,再将缓存删掉,等待下次主动查询在进行更新

    image.png image.png

    问题:会有读写,脏数据
    方案一:加锁
    方案二:如果经常写,少读,不如直接数据库操作,去掉缓存层。

    6、方案

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

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

    7、Canal(阿里) 解决缓存一致性问题

    需要上大数据系统衔接电商的时候


    image.png

    缓存读写一致性框架(Spring Cache)

    参考博客

    https://blog.csdn.net/weixin_44565095/article/details/100598965

    相关文章

      网友评论

          本文标题:Redission 分布式锁框架

          本文链接:https://www.haomeiwen.com/subject/mhyofrtx.html