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和缓存双写不一致问题。

分布式Redis解决方案之Redisson的更多相关文章

  1. Codis——分布式Redis服务的解决方案

    Codis——分布式Redis服务的解决方案 之前介绍过的 Twemproxy 是一种Redis代理,但它不支持集群的动态伸缩,而codis则支持动态的增减Redis节点:另外,官方的redis 3. ...

  2. [转载] Codis作者黄东旭细说分布式Redis架构设计和踩过的那些坑们

    原文: http://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=208733458&idx=1&sn=691bfde670fb ...

  3. 来吧,展示!Redis的分布式锁及其实现Redisson的全过程

    前言 分布式锁是控制分布式系统之间同步访问共享资源的一种方式. 在分布式系统中,常常需要协调他们的动作.如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要 ...

  4. [转帖]分布式锁-redLock And Redisson

    分布式锁-redLock And Redisson 2019-03-01 16:51:48 淹不死的水 阅读数 372更多 分类专栏: 分布式锁   版权声明:本文为博主原创文章,遵循CC 4.0 B ...

  5. 一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)

    这里是参考B站上的大佬做的面试题笔记.大家也可以去看视频讲解!!! 文章目录 1.分布式架构下,Session共享有什么方案 2.简述你对RPC.RMI的理解 3.分布式id生成方案 4.分布式锁解决 ...

  6. CRL快速开发框架系列教程六(分布式缓存解决方案)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

  7. 豌豆夹Redis解决方案Codis源码剖析:Dashboard

    豌豆夹Redis解决方案Codis源码剖析:Dashboard 1.不只是Dashboard 虽然名字叫Dashboard,但它在Codis中的作用却不可小觑.它不仅仅是Dashboard管理页面,更 ...

  8. 豌豆夹Redis解决方案Codis源码剖析:Proxy代理

    豌豆夹Redis解决方案Codis源码剖析:Proxy代理 1.预备知识 1.1 Codis Codis就不详细说了,摘抄一下GitHub上的一些项目描述: Codis is a proxy base ...

  9. 阿里开源分布式事务解决方案 Fescar

    微服务倡导将复杂的单体应用拆分为若干个功能简单.松耦合的服务,这样可以降低开发难度.增强扩展性.便于敏捷开发.当前被越来越多的开发者推崇,系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并 ...

  10. 分布式事务解决方案FESCAR

    项目地址:FESCAR 以下是官网的文档.简介2019年,Fescar 是 阿里巴巴 开源的 分布式事务中间件,以 高效 并且对业务 0 侵入 的方式,解决 微服务 场景下面临的分布式事务问题. 1. ...

随机推荐

  1. python 将数组写入文件

    简介 常用功能 code import numpy as np import time test_data = np.random.rand(6000000,12) T1 = time.time() ...

  2. OGSM 从上到下逐级分解策略:从战略目标到部门计划的标准化落地路径

    商界大战如火如荼,想要在竞争激烈.变幻莫测的市场环境中脱颖而出,战略规划和经营计划绝对必不可少! 别慌,OGSM(Objective, Goals, Strategies, Measures)就是从战 ...

  3. 模块化html代码

    HTML 页面模块化实现方案 将 HTML 页面进行模块化是前端开发中的常见需求,它可以提高代码的可维护性和复用性.针对你提供的这个建筑辅助设计工具集合页面,我们可以采用多种模块化方案,下面我将介绍几 ...

  4. C-Kermit AND C-Kermit for Android

    C-KERMIT 10.0 TUTORIAL https://www.kermitproject.org/ck10tutor.html#commands In the present age of g ...

  5. POLIR-Society-Organization-Politics-Self- Identity:Qualities + Habits:To-Be List + Behaviors:To-Do List + Prioritize: ProblemsResolving

    POLIR-Society-Organization-Political Habits:To-Be List Behaviors:To-Do List when it comes to habits, ...

  6. SciTech-Mathmatics-Statistics-Descriptive Statistics-"Pandas + NumPy" + "Best Ways to Grayscale/"Color Channels Split" Images with Python Using OpenCV+Pandas+NumPy

    问题:怎么解释 答案:percentile函数是统计学用于计算数据集的特定百分位数. percentile百分位数 与 percentile()函数 # 示原理代码 img = cv.imread(' ...

  7. css 美化原生的table样式

    先看效果图 对应的美化代码 css <style> table.gridtable { font-family: verdana,arial,sans-serif; font-size:1 ...

  8. J1939协议中的故障诊断

    目录 一.诊断故障码的组成 二.故障代码类型 2.1 DM1-当前故障代码 2.1.1 单帧DM1报文 2.1.2 多帧DM1报文 2.2 DM2-历史故障代码 2.3 DM3-历史故障诊断清除/复位 ...

  9. opencv实例练习(2)

    这里主要做一些图像几何变换的实例练习 Opencv提供了许多基础的图像变换函数,可以用于调整图像的大小.旋转.平移.裁剪等操作. 1.缩放图像 将图像对象缩小了一半并赋值给缩放后的图像 1 impor ...

  10. Macbook 忘记开机密码 重设密码

    Macbook Pro 忘记开机密码怎么办?而且指纹也没法用,因为提示只能先输入开机密码才能启用指纹 解决方法: 1. 首先重启电脑,在电脑开机的时候,马上按住 Command+R 组合键 2. 按住 ...