实践部分
实践代码来源:KnowledgePlanet / road-map / xfg-dev-tech-redis · GitCode
redisson 描述:1. 概述 · redisson/redisson Wiki · GitHub
- 可基于内存亦可持久化的日志型、Key-Value数据库,数据结构服务器因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
1. 数据缓存
分为数据插入、查询、更新等,Redis 的大部分操作其实都是缓存数据,提高系统的 QPS,在插入、更新、删除(逻辑删)、查询的时候,依赖于 Redis 进行提速操作。
-
在插入数据的时候,可以一并切入缓存。如果有更新操作,可以考虑删除缓存,在查询更新。因为更新操作,很多时候都是部分字段更新,这个时候直接更新缓存容易不准。
-
最后就是查询时,用缓存拦截,避免所有的查询都打到库上。这样可以提高系统的 QPS
-
另外关于缓存击穿,说的就是你本来要在缓存存放大量数据的,但存放偏差或者漏了,那么这个时候大量请求都打到库上,导致把数据库拖垮。尤其是那种需要做事务加锁有资源竞争的,会更严重
-
主要就是set get操作
//在基础层的仓储处理中,也就是调用数据库DAO操作过程中同时实现redis查询和缓存
@Override
public OrderEntity queryOrder(String orderId) {
OrderEntity orderEntity = redissonService.getValue(orderId);
if (null == orderEntity) {
UserOrderPO userOrderPO = userOrderDao.selectByOrderId(orderId);
orderEntity = OrderEntity.builder()
.userName(userOrderPO.getUserName())
.userId(userOrderPO.getUserId())
.userMobile(userOrderPO.getUserMobile())
.sku(userOrderPO.getSku())
.skuName(userOrderPO.getSkuName())
.orderId(userOrderPO.getOrderId())
.quantity(userOrderPO.getQuantity())
.unitPrice(userOrderPO.getUnitPrice())
.discountAmount(userOrderPO.getDiscountAmount())
.tax(userOrderPO.getTax())
.totalAmount(userOrderPO.getTotalAmount())
.orderDate(userOrderPO.getOrderDate())
.orderStatus(userOrderPO.getOrderStatus())
.uuid(userOrderPO.getUuid())
.build();
orderEntity.setDeviceVO(JSON.parseObject(userOrderPO.getExtData(), DeviceVO.class));
// 设置到缓存
redissonService.setValue(orderId, orderEntity);
}
return orderEntity;
}
2. 加锁处理
使用 Redis 加分布式锁,也是分布式架构设计中非常常用的手段。常用于的场景包括;流程较长,耗时较多的个人开户、下单行为。也包括;一些资源竞争时加分布式锁,排队处理请求。
但对于资源竞争的这类库存占用,如果加分布式锁是非常影响系统的吞吐量的,因为所有的用户都在等待上一个用户做完流程后释放锁的处理,相当于你即使系统是分布式的了,但这里的分布式锁依然会把性能拖慢。所以如图,我们要考虑两种场景不同的加锁方式
-
对于第1类的场景,主要是为了避免用户在一次操作后,又反复申请。系统上避免重复受理,所以添加分布式锁(独占锁)的方式进行拦截。如果不加分布式锁,就会进入到库表中通过唯一的索引拦截,这样对数据库的压力就比较大。
-
对于第2类的场景,是采用了分段或者自增滑块的锁方式进行处理,减少对同一个锁的等待,而是生成一堆的锁,让用户去使用。也就是最开始案例背景的图中,一个个⭕️圆圈的分段锁。
2.1 分布式锁
redis 分布式锁的 5个坑,真是又大又深 - YoungDeng - 博客园
Redis分布式锁—Redisson+RLock可重入锁实现篇 - niceyoo - 博客园
Java Jedis操作Redis示例(三)——setnx/getset实现分布式锁_java setnx-CSDN博客
解决分布式场景下的数据一致性问题;分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
乐观锁和悲观锁
- 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
- 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁
针对分布式锁的实现,目前比较常用的有以下几种方案:
-
基于数据库实现分布式锁
-
基于缓存(redis,memcached,tair)实现分布式锁
-
基于Zookeeper实现分布式锁
2.2 redis相关加锁实现
setNX + Lua脚本
Redis分布式锁—SETNX+Lua脚本实现篇 - niceyoo - 博客园
SET if Not eXists 的简写 setnx
SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
必选参数说明:
SET:命令
key:待设置的key
value:设置的key的value,最好为随机字符串
可选参数说明:
NX:表示key不存在时才设置,如果存在则返回 null
XX:表示key存在时才设置,如果不存在则返回NULL
PX millseconds:设置过期时间,过期时间精确为毫秒
EX seconds:设置过期时间,过期时间精确为秒
Redis 2.6.12 版本之后,Redis 支持原子命令加锁,
我们可以通过向 Redis 发送 「set key value NX 过期时间」 命令,实现原子的加锁操作;
返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL key 来释放该锁。
如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。
Redisson + RLock可重入锁
Redisson 是 java 的 Redis 客户端之一,是 Redis 官网推荐的 java 语言实现分布式锁的项目。是在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。Redisson底层采用的是[Netty](http://netty.io/) 框架。支持[Redis](http://redis.cn) 2.8以上版本,支持Java1.6+以上版本。
Redisson 提供了一些 api 方便操作 Redis。因为本文主要以锁为主,所以接下来我们主要关注锁相关的类,以下是 Redisson 中提供的多样化的锁:是目前大部分公司使用 Redis 分布式锁最常用的一种方式。
- 可重入锁(Reentrant Lock 即RLock Java 对象)
- 公平锁(Fair Lock)
- 联锁(MultiLock)
- 红锁(RedLock)
- 读写锁(ReadWriteLock)
- 信号量(Semaphore)
- 闭锁(CountDownLatch)
- 过期锁(Lease Lock)
pom.xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.4</version>
</dependency>
redisson配置类
/**
* @author Fuzhengwei bugstack.cn @小傅哥
* @description Redis 客户端,使用 Redisson <a href="https://github.com/redisson/redisson">Redisson</a>
*rties(RedisClientConfigProperties是一个类一一对应于
@create 2023-09-09 16:51
*/
@Configuration
@EnableConfigurationProperties(RedisClientConfigProperties.class)
public class RedisClientConfig {
@Bean("redissonClient")
public RedissonClient redissonClient(ConfigurableApplicationContext applicationContext, RedisClientConfigProperties properties) {
Config config = new Config();
// 根据需要可以设定编解码器;https://github.com/redisson/redisson/wiki/4.-%E6%95%B0%E6%8D%AE%E5%BA%8F%E5%88%97%E5%8C%96
// config.setCodec(new RedisCodec());
config.useSingleServer()
.setAddress("redis://" + properties.getHost() + ":" + properties.getPort())
.setPassword(properties.getPassword())
.setConnectionPoolSize(properties.getPoolSize())
.setConnectionMinimumIdleSize(properties.getMinIdleSize())
.setIdleConnectionTimeout(properties.getIdleTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setRetryAttempts(properties.getRetryAttempts())
.setRetryInterval(properties.getRetryInterval())
.setPingConnectionInterval(properties.getPingInterval())
.setKeepAlive(properties.isKeepAlive())
;
RedissonClient redissonClient = Redisson.create(config);
String[] beanNamesForType = applicationContext.getBeanNamesForType(MessageListener.class);
for (String beanName : beanNamesForType) {
MessageListener bean = applicationContext.getBean(beanName, MessageListener.class);
Class<? extends MessageListener> beanClass = bean.getClass();
if (beanClass.isAnnotationPresent(RedisTopic.class)) {
RedisTopic redisTopic = beanClass.getAnnotation(RedisTopic.class);
RTopic topic = redissonClient.getTopic(redisTopic.topic());
topic.addListener(String.class, bean);
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
beanFactory.registerSingleton(redisTopic.topic(), topic);
}
}
return redissonClient;
}
/**
* 手动配置
*/
@Bean("testRedisTopic")
public RTopic testRedisTopicListener(RedissonClient redissonClient, RedisTopicListener01 redisTopicListener) {
RTopic topic = redissonClient.getTopic("xfg-dev-tech-topic");
topic.addListener(String.class, redisTopicListener);
return topic;
}
}
属性配置____
/**
* @author Fuzhengwei bugstack.cn @小傅哥
* @description Redis 连接配置
* <a href="https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter">redisson-spring-boot-starter</a>
* 对应于application.yml中的关于redis的配置,
*
* @create 2023-09-03 16:51
*/
@Data
@ConfigurationProperties(prefix = "redis.sdk.config", ignoreInvalidFields = true)
public class RedisClientConfigProperties {
/** host:ip */
private String host;
/** 端口 */
private int port;
/** 账密 */
private String password;
/** 设置连接池的大小,默认为64 */
private int poolSize = 64;
/** 设置连接池的最小空闲连接数,默认为10 */
private int minIdleSize = 10;
/** 设置连接的最大空闲时间(单位:毫秒),超过该时间的空闲连接将被关闭,默认为10000 */
private int idleTimeout = 10000;
/** 设置连接超时时间(单位:毫秒),默认为10000 */
private int connectTimeout = 10000;
/** 设置连接重试次数,默认为3 */
private int retryAttempts = 3;
/** 设置连接重试的间隔时间(单位:毫秒),默认为1000 */
private int retryInterval = 1000;
/** 设置定期检查连接是否可用的时间间隔(单位:毫秒),默认为0,表示不进行定期检查 */
private int pingInterval = 0;
/** 设置是否保持长连接,默认为true */
private boolean keepAlive = true;
}
RLock可重入锁指同一个线程可以多次获得同一个锁,而不会发生死锁。
基于 RLock 接口,而 RLock 锁接口实现源码主要是 RedissonLock 这个类,而源码中加锁、释放锁等操作都是使用 Lua 脚本来完成的,并且封装的非常完善,开箱即用.
- 若负责存储分布式锁的Redisson对象宕机且这个锁处于锁住状态就会出现锁死状态,为避免这个情况,redisson提供了监控锁的看门狗watchDog(在Redisson实例被关闭前,不断的延长锁的有效期,默认30秒)
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();
- 还可以利用参数来指定加锁时间:
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
- 还有异步执行的方法:
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
说明: RLock
对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException
错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore
对象.
分布式锁原理源码追溯:https://segmentfault.com/a/1190000041495173
以上代码都是Rlock 先通过getLock(lock_name_key)获取实现可重入分布式锁的类,
lock.lock()
加锁,尝试加锁,未加锁成功的线程订阅Redis的消息, 未加锁成功的线程通过自旋获取锁
lock.unlock()
解锁,释放锁,取消到期续订;底层是lua脚本实现
redisson基于了redis做的一个分布式锁,使用了类似redis的set key value nx命令的脚本,做的一个原子性建锁操作:如果锁不存在,则设置锁,并返回1(Long类型);如果锁存在,这返回0,锁存在,就代表着,有线程获取到了锁,并正在执行任务,其他的线程,会进入阻塞状态,在外部等待(独占式锁就是利用了这点)。
// 无锁化或者分段锁或者非独占式锁,
// 核心在于处理有资源竞争的业务,例如库存秒杀-所有的用户都需要竞争
//逻辑在于 每个线程进入到逻辑都可以去先将库存减一,
//而不是必须等前一个线程执行完业务再锁定库存
public String createOrderByNoLock(OrderAggregate orderAggregate) {
SKUEntity skuEntity = orderAggregate.getSkuEntity();
// 以下是 模拟锁商品库存
// 先对sku 关键字 decr 减去一个库存;
long decrCount = redissonService.decr(skuEntity.getSku());
if (decrCount < 0) return "已无库存[初始化的库存和使用库存,保持一致。orderService.initSkuCount(\"13811216\", 10000);]";
// 设置 锁关键字 sku_当前减少一个后的库存数量 每个锁一定要加上关键字和数据下量_
String lockKey = skuEntity.getSku().concat("_").concat(String.valueOf(decrCount));
// RLock:redis锁对象 获得该锁
RLock lock = redissonService.getLock(lockKey);
try {
// 加锁处理
lock.lock();
return createOrder(orderAggregate);
} finally {
lock.unlock();
}
}
//独占式锁 意味着所有的业务逻辑必须要等着前一个线程都执行完才能执行
//
public String createOrderByLock(OrderAggregate orderAggregate) {
RLock lock = redissonService.getLock("create_order_lock_".concat(orderAggregate.getSkuEntity().getSku()));
try {
lock.lock();//谁先到这里,谁先锁住,其他判断被锁了,只能不断自旋尝试获取锁
long decrCount = redissonService.decr(orderAggregate.getSkuEntity().getSku());//锁了以后再执行业务逻辑;
if (decrCount < 0) return "已无库存[初始化的库存和使用库存,保持一致。orderService.initSkuCount(\"13811216\", 10000);]";
return createOrder(orderAggregate);
} finally {
lock.unlock();
}
}
3. 发布订阅
6. 分布式对象 - 6.7. 话题(订阅分发) - 《Redisson 使用手册》 - 书栈网 · BookStack
ps: 消息队列—— rabbitmq等消息中间件,而少用redis的发布订阅。
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。
方式一:subscri + publish
gif 演示如下:
-
开启本地 Redis 服务,开启两个 redis-cli 客户端。
-
在第一个 redis-cli 客户端输入 SUBSCRIBE runoobChat,意思是订阅
runoobChat
频道。 -
在第二个 redis-cli 客户端输入 PUBLISH runoobChat “Redis PUBLISH test” 往 runoobChat 频道发送消息,这个时候在第一个 redis-cli 客户端就会看到由第二个 redis-cli 客户端发送的测试消息。
优点:支持多端订阅、简单、性能高
缺点:数据会丢失
//blpop + lpush
//程序1:使用代码实现订阅端
while (running) {
try {
var msg = RedisHelper.BLPop(5, "list1");
if (string.IsNullOrEmpty(msg) == false) {
Console.WriteLine(msg);
}
} catch (Exception ex) {
Console.WriteLine(ex.Message);
}
}
//程序2:使用代码实现发布端
RedisHelper.LPush("list1", "111");
- 优势:数据不会丢失、简单、性能高;缺点:不支持多端(存在资源争抢);总结:为了解决方法一的痛点,我们实现了本方法,并且很漂亮的制造了一个新问题(不支持多端订阅)。
Java boot + redis 发布订阅
redisson 提供了一种简易实现的订阅发布,适用于访问量不大的情况,否则会使内存负担较重。
Redisson的分布式话题RTopic
,
简单实现代码:
// 创建Redisson客户端
RedissonClient redisson = Redisson.create();
// 获取RTopic对象
RTopic<String> topic = redisson.getTopic("myTopic");
// 发布消息
topic.publish("Hello, Redisson!");
// 添加监听器
topic.addListener(String.class, (channel, msg) -> {
System.out.println("Received message: " + msg);
});
// 关闭Redisson客户端
redisson.shutdown();
- 发布和订阅实现基于 Redis 的事件机制,即订阅者通过执行 SUBSCRIBE 命令将自己的监听器添加到 Redis 服务器维护的事件循环器中,当发布者通过 PUBLISH 命令向指定频道发送消息时,Redis 服务器会将消息发送给监听该频道的所有订阅者;
- 在Redis节点故障转移(主从切换)或断线重连以后,所有的话题监听器将自动完成话题的重新订阅。
- 发布订阅是我们需要对同一个 Topic 进行发布和监听操作。但这个操作的代码是一种手动编码。实际开发中,改变手动编码模式,通过自定义注解,来完成动态监听和将对象动态注入到 Spring 容器中,让需要注入的属性,可以被动态注入.
- 3.1.1 自定义注解
package cn.bugstack.xfg.dev.tech.types;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
public @interface RedisTopic {
String topic() default "";
}
- 3.1.2 注解使用
@Slf4j
@Service
@RedisTopic(topic = "testRedisTopic02")
public class RedisTopicListener02 implements MessageListener<String> {
@Override
public void onMessage(CharSequence channel, String msg) {
log.info("02-监听消息(Redis 发布/订阅): {}", msg);
}
}
- 3.1.3 动态注入
// 添加监听
String[] beanNamesForType = applicationContext.getBeanNamesForType(MessageListener.class);
for (String beanName : beanNamesForType) {
MessageListener bean = applicationContext.getBean(beanName, MessageListener.class);
Class<?> beanClass = bean.getClass();
if (beanClass.isAnnotationPresent(RedisTopic.class)) {
RedisTopic redisTopic = beanClass.getAnnotation(RedisTopic.class);
RTopic topic = redissonClient.getTopic(redisTopic.topic());
topic.addListener(String.class, bean);
// 动态创建 bean 对象,注入到 spring 容器,bean 的名称为 redisTopic,对象为 RTopic
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
beanFactory.registerSingleton(redisTopic.topic(), topic);
}
}
- 3.1.4 使用
@Resource(name = "testRedisTopic02")
private RTopic testRedisTopic02;
@Resource(name = "testRedisTopic03")
private RTopic testRedisTopic03;
@Override
public String createOrder(OrderAggregate orderAggregate) {
// 省略...
testRedisTopic02.publish(JSON.toJSONString(orderEntity));
testRedisTopic03.publish(JSON.toJSONString(orderEntity));
return orderId;
}