1.数据库与缓存一致性方案
2.热key探测系统处理热key问题
3.缓存大value监控和切分处理方案
4.Redis内存不足强制回收监控告警方案
5.Redis集群缓存雪崩自动探测 + 限流降级方案
6.缓存击穿的解决方法
线上Redis比较严重的问题排序是:数据库和缓存一致性、热key、大value、缓存雪崩限流降级、内存不足强制回收
1.数据库与缓存一致性方案
(1)数据库与缓存同步双写强一致性方案
(2)数据库与缓存异步同步最终一致性方案
现有的业务场景下,都会涉及到数据库以及缓存双写的问题。⽆论是先删缓存再更新数据,或先更新数据再删缓存,都⽆法保证⼀致性。本身它们就不是⼀个数据源,⽆法通过代码上的谁先谁后去保证顺序。
(1)数据库与缓存同步双写强一致性方案
这是适合中小企业的方案:读数据时自动进行读延期,实现数据冷热分离。在保证数据库和缓存一致性时使用分布式锁,第一个获得分布式锁的线程双写数据库和缓存成功后才释放分布式锁。然后在高并发下,通过锁超时时间,实现"串行等待分布式锁 + 串行读缓存"转"串行读缓存"。
(2)数据库与缓存异步同步最终一致性方案
如果不想对数据库和缓存进行双写,可以监听数据库binlog日志,通过异步来进行数据复制同步,从而保证数据的最终一致性。
这个方案需要先写成功DB,之后才能读到缓存value。这个方案需要确保binlog不能丢失,并且需要使用Canal监听binlog。
一.具体的数据一致性方案设计
⾸先对于所有的DB操作都不去添加具体的删除缓存操作,⽽是待数据确认已提交到数据库后,通过Canal去监听binlog的变化。
Canal会将binlog封装成消息发送到MQ,然后系统消费MQ的消息时,需要过滤出增删改类型的binlog消息。接着根据binlog消息 + 一致性相关的表和字段组装需要进行缓存删除的key,最后组装出key就可以对缓存进行删除了。
相关文章:
https://book.qq.com/book-search/%E5%90%8D%E4%BC%98%E9%A6%86%E7%BD%91%E3%80%9023Y4.com%E3%80%91?a21a
https://book.qq.com/book-search/%E6%B5%B7%E8%A7%92%E7%A4%BE%E5%8C%BA%E3%80%9023Y4.com%E3%80%91?b21b
https://book.qq.com/book-search/%E8%89%B3%E6%AF%8D%E7%BD%91%E8%BF%9B%E3%80%9023Y4.com%E3%80%91?c21c
https://book.qq.com/book-search/%E6%9E%9C%E5%86%BB%E4%BC%A0%E5%AA%92%E3%80%9023Y4.com%E3%80%91?d21d
https://book.qq.com/book-search/%E6%9E%9C%E5%86%BB%E4%BC%A0%E5%AA%92%E8%BF%9B23Y4.com%E7%9C%8B?e21e
https://book.qq.com/book-search/%E6%80%A7%E5%B7%B4%E5%85%8B%E8%BF%9B%E3%80%9023Y4.com%E3%80%91?f21f
https://book.qq.com/book-search/%E7%88%B1%E5%A8%81%E5%A5%B6%E7%BD%91%E3%80%9023Y4.com%E3%80%91?g21g
https://book.qq.com/book-search/%E7%A6%81%E6%BC%AB%E5%A4%A9%E5%A0%82%E3%80%9023Y4.com%E3%80%91?c21a
https://book.qq.com/book-search/%E6%92%B8%E6%92%B8%E7%A4%BE%E7%BD%91%E3%80%9023Y4.com%E3%80%91?a21b
https://book.qq.com/book-search/%E6%8A%96%E9%98%B4%E4%B8%8B%E8%BD%BD%E3%80%8A23Y4.com%E3%80%8B?b21c
https://book.qq.com/book-search/%E6%8A%96%E9%98%B4%E7%BD%91%E7%AB%99%E3%80%9023Y4.com%E3%80%91?c21d
二.具体的数据一致性方案流程图
三.处理MQ消息保证最终数据一致性
//处理MQ消息保证最终数据一致性
@Slf4j
@Component
public class CookbookConsistencyListener implements MessageListenerConcurrently {
@Autowired
private RedisCache redisCache;
//处理MySQL的binlog变化,处理需要清理的缓存key
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
try {
for (MessageExt messageExt : list) {
String msg = new String(messageExt.getBody());
//解析binlog数据模型,并过滤掉查询
BinlogDataDTO binlogData = buildBinlogData(msg);
//获取binlog的模型,获取本次变化的表名称,在本地配置常量类里面匹配对应的缓存key前缀以及缓存标识字段,非配置的表不进行处理
String cacheKey = filterConsistencyTable(binlogData);
//删除该key的缓存
deleteCacheKey(cacheKey);
}
} catch (Exception e) {
log.error("consume error, 缓存清理失败", e);
//本次消费失败,下次重新消费
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
//解析binlog的数据模型,并过滤掉查询的binlog
private BinlogDataDTO buildBinlogData(String msg) {
//先解析binlog的对象,转换为模型
BinlogDataDTO binlogData = BinlogUtils.getBinlogData(msg);
//模型为null,则直接返回
if (Objects.isNull(binlogData)) {
return null;
}
Boolean isOperateType = BinlogType.INSERT.getValue().equals(binlogData.getOperateType())
|| BinlogType.DELETE.getValue().equals(binlogData.getOperateType())
|| BinlogType.UPDATE.getValue().equals(binlogData.getOperateType());
//只保留增删改的binlog对象,如果数据对象为空则也不处理
if (!isOperateType || CollectionUtils.isEmpty(binlogData.getDataMap())) {
return null;
}
//返回解析好的可用模型
return binlogData;
}
//过滤掉目前不需要处理的表的Binlog,并返回组装所需的缓存key
private String filterConsistencyTable(BinlogDataDTO binlogData) {
if (Objects.isNull(binlogData)) {
return null;
}
String tableName = binlogData.getTableName();
List<Map<String, Object>> dataList = binlogData.getDataMap();
//获取配置的常量映射的具体配置
ConsistencyTableEnum consistencyTableEnum = ConsistencyTableEnum.findByEnum(tableName);
if (Objects.isNull(consistencyTableEnum)) {
return null;
}
String cacheValue = "";
if (CollectionUtils.isNotEmpty(dataList)) {
Map<String, Object> dataMap = dataList.get(0);
cacheValue = dataMap.get(consistencyTableEnum.getCacheField()) + "";
}
if (StringUtils.isBlank(cacheValue)) {
return null;
}
//获取配置的缓存前缀key + 当前的标识字段,组装缓存key
return consistencyTableEnum.getCacheKey() + cacheValue;
}
//对缓存进行清理
private void deleteCacheKey(String cacheKey) {
if (StringUtils.isBlank(cacheKey)) {
return;
}
redisCache.delete(cacheKey);
}
}
public enum ConsistencyTableEnum {
//商品表缓存配置
SKU_INFO("sku_info", RedisKeyConstants.GOODS_INFO_PREFIX, "id");
//配置相关的表名称
private final String tableName;
//缓存的前缀key
private final String cacheKey;
//缓存的标识字段
private final String cacheField;
...
}
2.热key探测系统处理热key问题
(1)什么是热key问题
(2)如何解决热key问题
(3)开源热key探测系统的工作流程图和架构图
当系统大量使用Redis进行开发后,线上必然会遇到热key和大value问题。
(1)什么是热key问题
比如微博大热点,瞬间千万级大流量都会涌入某微博来浏览某个数据。这时如果这个热点数据是存储在一个Redis节点里,那么就会出现每秒百万级的请求都到一个Redis节点去了。如下图示:
(2)如何解决热key问题
为了解决热key问题,我们首先需要一个热key探测系统。热key探测系统会在服务系统(Redis客户端)进行接入统计,一旦热key探测系统在服务系统识别出某个key符合热key的条件,那么就会将这个热key的数据缓存到服务系统的JVM本地缓存里。
所以,热key探测系统具备的两大核心功能:
一.自动探测热key
二.自动缓存热key数据到JVM本地缓存
(3)热key探测系统的工作流程图和架构图
一.热key探测系统的工作流程简图
二.热key探测系统的详细架构图
3.缓存大value监控和切分处理方案
大value,顾名思义,就是value值特别大,几M甚至几十M。如果在一次网络读取里面,频繁读取大value,会导致网络带宽被占用掉。value太大甚至会把带宽打满,导致其他数据读取请求异常。所以对于大value,要进行特殊的切分处理。
一.首先要能够对Redis里的大value进行监控。如果发现超过1MB的大value 的值,就监控和报警。
二.然后进行自动处理,也就是把这个value的值,转换为拆分字符串来缓存。比如将一个大value拆分为10个串,把一个kv拆分为10个kv来存储:test_key => test_key_01、test_key_02...
三.接着进行读取的时候,则依次读取这拆分字符串对应的key,最后将读出来的拆分字符串进行重新拼接还原成原来的大value值。