• 分布式Redis解决方案之Redisson


    1.前言

    Redisson是Redis官方推荐的Java版的Redis客户端。底层使用netty框架,并提供了与java对象相对应的分布式对象、分布式集合、分布式锁和同步器、分布式服务等一系列的Redisson的分布式对象。

    2.使用准备

    1)导入依赖

    <dependency>
           <groupId>org.redisson</groupId>
           <artifactId>redisson-spring-boot-starter</artifactId>
           <version>3.11.6</version>
    </dependency>

    2)基本配置

    在application.yml内容如下:(注意,一定要使用yml方式,否则在下面的redisson-config.yaml中通过${}获取会失败)

    # redis基本配置
    spring:
      redis:
      host: 127.0.0.1
      port: 6379
      #Redisson配置 配置配置文件路径
      redisson:
       config: classpath:redisson-config.yaml

    配置redisson,在资源目录下新建redisson-config.yaml,内容如下:

    #单节点模式配置
    singleServerConfig:
      #节点地址
      address: redis://${spring.redis.host}:${spring.redis.port}
      #密码
      password: null
      #发布和订阅连接的最小空闲连接数
      subscriptionConnectionMinimumIdleSize: 1
      #发布和订阅连接池大小
      subscriptionConnectionPoolSize: 50
      #最小空闲连接数
      connectionMinimumIdleSize: 32
      #连接池大小
      connectionPoolSize: 64
      #数据库编号 0-15
      database: 0
      #连接空闲超时,单位:毫秒
      idleConnectionTimeout: 10000
      #连接超时,单位:毫秒
      connectTimeout: 10000
      #命令等待超时,单位:毫秒
      timeout: 3000
      #命令失败重试次数
      retryAttempts: 3
      #命令重试发送时间间隔,单位:毫秒
      retryInterval: 1500
      #单个连接最大订阅数量
      subscriptionsPerConnection: 5
      #客户端名称
      clientName: null
    #线程池数量
    threads: 0
    #Netty线程池数量
    nettyThreads: 0
    #对象编码
    codec: !<org.redisson.codec.JsonJacksonCodec> { }
    #传输模式
    transportMode: NIO
    
    #集群配置 出节点地址外,其他同单节点配置
    #clusterServersConfig:
    #  nodeAddresses:
    #    - "地址1"
    #    - "地址2"

    3.分布式锁

    3.1情景模拟

    模拟减库存的场景,先在redis中对商品id为1001设置库存(key为product:pro_1001:count,value500),减库存代码如下:

        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @GetMapping("/test")
        public void test() {
            String key = "product:pro_1001:count";
            BoundValueOperations<String, String> ops = redisTemplate.boundValueOps(key);
            Integer cnt = Integer.parseInt(ops.get());
            if (cnt >= 0) {
                cnt--;
                ops.set(cnt.toString());
                log.info("扣减成功" + cnt);
            } else {
                log.info("存库不足");
            }
        }

    复制两个应用并启动,使用nginx进行代理,然后用Jmeter模拟并发请求,在同一时间内并发200次,循环2次

    在控制台可看到日志中出现了库存存在相同的情况

    原因分析:当多个并发请求同时操作redis数据库时,可能一个请求读取到的库存是500,此时开始减一操作,另一个请求也开始读取库存,此时库存还未还更新完成,任然是500,此请求也是对500减一操作,那么这样就会出现数据错乱,引起超卖问题。

    3.2解决方案

    可使用分布式锁来解决,当一个请求处理完成后再处理其他请求,单线程按序执行。

        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Autowired
        private RedissonClient redissonClient;
    
        @GetMapping("/test")
        public void test() {
            String key = "product:pro_1001:count";
            RLock redissonClientLock = redissonClient.getLock(key);
            redissonClientLock.lock();//上锁
            try {
                BoundValueOperations<String, String> ops = redisTemplate.boundValueOps(key);
                if (ops.get() != null) {
                    Integer cnt = Integer.parseInt(ops.get());
                    if (cnt >= 0) {
                        cnt--;
                        ops.set(cnt.toString());
                        log.info("扣减成功" + cnt);
                    } else {
                        log.info("存库不足");
                    }
                } else {
                    log.info("存库不足");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                redissonClientLock.unlock();//释放锁
            }
        }

    在进行业务处理时时,先进行上锁操作,处理完成后无论正常与否都需释放锁。

    3.3更多应用场景

    其分布式锁的应用场景是非常多的,除了商品减库存外,还可用以下方面:

    • 突发热点数据。前面也讲到,缓存中存储的都是热点数据,但不排除某非热点数据因个别因素瞬间变为热点数据。那么当多个并发请求在到达后端时,由于缓存中不存在,便会直奔数据库,给数据库造成巨大的压力。解决问题的方式就是利用redis的分布式锁,在向数据库查询数据时,先对第一个请求上锁,待第一个请求查询完成存入缓存后,后续的请求才能继续查询。那么此时要查询的数据已变为热点数据,后续请求即可从缓存中获取数据而不用再从数据库获取。

    3.4读写锁

    在高并发情况下,通常更新完db后再去更新缓存,不加锁显而易见会出现缓存被覆盖的问题:线程1修改完db去更新缓存时卡顿了一下。此时线程2在线程1之后修改完db并成功更新了缓存。此时线程1更新缓存的操作恢复了,然后去更新了缓存。那么此时的缓存是脏数据,应为线程2缓存的数据,但实际上是线程1缓存的数据。也就是写操作出现了脏数据,使用读写锁可解决此问题。

    读写锁,一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁,从而保证了数据的一致性。优点如下:

    当读写锁在 写 加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞;

    当读写锁在 读 加锁状态时,所有试图以读模式对它进行加锁的线程都可得到访问权,但是若线程以写模式对此锁进行加锁,则它会被阻塞,直到所有的线程释放锁后才能进行加锁。

    换句话说,只要涉及到写锁,则都会阻塞,如果是先写再读,则读锁等待,如果是先读再写,则写锁等待。

    代码如下:

        private static final String READ_WRITE_LOCK = "readWrite";
        private static final String READ_WRITE_KEY = "test:uuid";
    
        @GetMapping("/read")
        public String read() {
            String s = null;
            //读写锁
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(READ_WRITE_LOCK);
            //读之前加锁,若该锁已被写锁锁定,则需等待其释放后才能读取
            RLock rLock = readWriteLock.readLock();
            try {
                rLock.lock();
                Thread.sleep(20000);
                s = redisTemplate.opsForValue().get(READ_WRITE_KEY);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                rLock.unlock();
            }
            return s;
        }
    
        @GetMapping("/write")
        public String write() {
            String s = null;
            //读写锁
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(READ_WRITE_LOCK);
            //写之前加锁,若该锁已被读锁锁定,则需等待其释放后才能写入
            RLock wLock = readWriteLock.writeLock();
            try {
                wLock.lock();
                Thread.sleep(20000);
                s = UUID.randomUUID().toString();
                redisTemplate.opsForValue().set(READ_WRITE_KEY, s);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                wLock.unlock();
            }
            return s;
        }

    启动后使用浏览器进行测试,为了演示效果,这里使用20秒延时

    1)只访问read请求,访问到的是最新的数据

    2)只访问write请求,正常写入数据

    3)先访问read请求,再快速访问write请求,通过日志会发现write请求在等待中,直到read请求处理完成才响应write请求

    4)先访问write请求,再快速访问read请求,通过日志会发现read请求在等待中,直到write请求处理完成才响应read请求

    通过以上方式,会出现读写不一致情况,但可保证redis写入的数据是最新的,解决了db和缓存双写不一致问题。

  • 相关阅读:
    webpack打包的项目,如何向项目中注入一个全局变量
    移动端微信H5兼容ios的自动播放音视频
    移动端H5解决键盘弹出时之后滚动位置发生变化的问题
    微信网页开发,如何在H5页面中设置分享的标题,内容以及缩略图
    React实现组件缓存的一种思路
    React编写一个移动H5的纵向翻屏组件
    如何手写一个react项目生成工具,并发布到npm官网
    Puppeteer爬取单页面网站的数据示例
    modelsim中objects窗口为空的解决办法
    Lattice Diamond与modelsim联合仿真环境设置
  • 原文地址:https://www.cnblogs.com/zys2019/p/16401233.html
Copyright © 2020-2023  润新知