一.本地缓存的前沿背景和使用场景
1.1.业务背景
在互联网-电商这个业务模块,特别C端模块,整体用户基数大,特别是在做营销活动的场景,甚至做秒杀活动的时候,访问量很高,而且C端是典型的大部分数据是多读少写场景,所以需要扛高并发,并且需要支持高性能,其中一个主要实现之一:添加策略缓存,在整个访问全链路上,各个地方添加缓存,对我们后端同学来说,常见的数据可能放入分布式缓存,甚至放入应用的本地缓存中,以便支持高吞吐量,低延迟的业务需求。
1.2.本地缓存的常见使用场景
数据量不是很大
非线形一直增长
修改频率较低
实时性要求较低
甚至是部分相对静态的数据
查询QPS要求极高
通过纯内存操作,避免网络I/O开销请求
例如:秒杀热点商品缓存、地域信息缓存、行政区信息缓存、常规的枚举数据缓存等
二.本地缓存的本身常见要点和方案实现
2.1.主要考虑的要点
支持key/value 形式的数据结构
支持线程安全
支持缓存过期时间
支持缓存淘汰策略
支持缓存大小控制
支持性能处理方面
支持统计和可观察性
2.2.方案实现之一ConcurrentHashMap
支持key/value 形式的数据结构(支持)
支持线程安全(支持)
缓存过期时间(本身不支持,需要额外单独实现)
缓存淘汰策略(本身不支持,需要额外单独实现)
缓存大小控制(本身不支持,需要额外单独实现)
2.3.方案实现之一Guava-Cache
支持key/value 形式的数据结构(支持)
支持线程安全(支持)
缓存过期时间 (支持,常见使用惰性删除,读请求中混杂着写操作)
缓存淘汰策略(支持,常见使用LRU算法)
缓存大小控制(支持)
2.4.方案实现之一Caffeine
支持key/value 形式的数据结构(支持)
支持线程安全(支持)
缓存过期时间(支持,使用异步淘汰数据的策略,缩短get请求的执行时长,间接提升了响应性能)
缓存淘汰策略(支持,使用一种高效的近似LFU算法,W-TinyLFU算法 特点:高命中率、低内存占用【利用堆外缓存降低内存缓存大小,减少GC频率】)
缓存大小控制(支持)
近似统计频率(采用 Count–Min Sketch算法【类似布隆过滤器】降低频率信息带来的内存消耗)支持热点缓存(支持,维护一个PK机制保证新进入的热点数据能够缓存)
支持异步(支持,eg:支持自动异步回源)
性能优化方面(很多地方采用了异步)
2.5.推荐的使用姿势和包含的相关功能
通过统一的SDK封装支持(目标:尽量和业务解耦;方便接入;统一的可维护性)
接入方式:AOP(可以融合多种缓存能力支持:分布式缓存 OR 本地缓存 OR 组合使用)
本地缓存:推荐选型Caffeine
KEY:使用SpEL动态表达式
可支持动态调整参数【例如:通过Apollo上配置动态支持】
支持缓存降级:支持降级
支持统计各业务key类型的命中率:支持
支持统计各业务key类型的QPS:支持
三.本地缓存的数据一致性
3.1.数据一致性的业务背景
正常的使用缓存的场景,多数是多读少写,我们可以支持TTL的设置,
在缓存失效的时候我们做异步回源,不过有部分业务需求,需要更高的强实效性和数据一致性
例如B端有相关的变更,部分业务场景,我们需要及时的支持数据一致性的闭环。
例如:强实效性的业务数据,eg:定时发布页面功能,修改敏感信息等
本地缓存数据一致性主要交互流程
3.2.实现之一-利用发布订阅模式
3.2.1 Redis的发布订阅模式
发布/订阅是一种消息模式,消息的发送者不会将消息直接发送给特定的接收者,而是通过消息通道广播出去,让订阅该消息主题的订阅者消费到,到达解耦
下面是主要相关伪代码参考示例
生产者:发布消息-主要部分代码示例参考
/**
* 生产者:发布消息-代码示例
* @param channel:通道:类似topic(CommonCacheConstant.CHANGE_CHANNEL)
* @param message:信息本体
*/
redisTemplate.convertAndSend(String channel, Object message);
消费者:监听消息和处理消息-主要部分代码示例参考
/**
* 消息者:订阅消息-代码示例,通过Redis消息监听容器(加载了RedisConnectionFactory和MessageListenerAdapter)
* @param RedisConnectionFactory:redis链接工厂
* @param ChangeListener(MessageListenerAdapter):消息监听器
*/
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,ChangeListener changeListener)
{
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(changeListener, new PatternTopic(CommonCacheConstant.CHANGE_CHANNEL));
return container;
}
/**
* 消息监听器-代码示例
*/
public class ChangeListener extends MessageListenerAdapter implements MessageListener
{
/**
* 处理目标消息
*/
@Override
public void onMessage(Message message, @Nullable byte[] pattern)
{
String partKey = message.toString();
//log.info("更新清除本地缓存:"+ partKey);
Set<String> deleteKeySet = new HashSet<>();
//TODO 封装deleteKeySet
//例如:删除目标的本地缓存
caffeineClient.invalidateAll(deleteKeySet);
}
}
3.2.2 MQ的广播模式
redis的发布的消息,接入使用相对比较简单,不过也有些缺点:例如:不会持久化,消息有被丢失的风险等
若对数据一致性要求比较高的业务场景,建议可以使用MQ的类似相关功能
很多MQ都有类似功能,我们以RocketMQ为示例-使用其中的广播模式
下面是主要伪代码参考示例
生产者:发送消息-主要部分代码示例参考