【缓存系列】一种能彻底解决缓存三大难题的缓存方案
在【缓存系列】几种缓存读写方案的优缺点和选择中我们介绍了传统的几种缓存读写方案,并分析了它们适合在什么场景下使用,会遇到什么样的问题,以及怎么去解决。
而在并发较高的情况下,甚至会造成更为严重的问题,主要有三大类,缓存雪崩、击穿以及穿透,定义和解决方案可以参考之前的文章【缓存系列】彻底解决缓存雪崩、击穿、穿透问题。
仔细思考一下它们产生的原因,本质上都是由于读操作中的"获取缓存-如果没有-则查DB"的逻辑导致的,那如果对于某些极度依赖缓存的场景,是不是可以换一种思路,直接以缓存为主数据源,即查询缓存,有直接返回,没有也不会去查DB了。缓存的更新通过监听数据库的变更记录实现,比如监听Mysql的binlog,可将变更记录投递到 MQ 系统中,例如 Kafka/RocketMQ。
我们来具体看下这个方案。
实现方案
写
增量
- 先更新DB;
- 监听binlog消息实时更新Cache;
存量
- 全量数据同步task,用于上线前全量初始化Cache数据
- 为保证binlog消息可能丢失带来的影响,初始化task每半天跑一次
读
- 从Cache中读数据
- 直接返回读取的数据(Cache中没有的数据默认表示DB中的数据也没有)
关于监听Mysql数据变更的binlog之后发送到消息队列中的功能,可以使用一些现有的组件来配合实现,例如阿里巴巴开源的canal,
其原理大致如下:
- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
- canal 解析 binary log 对象(原始为 byte 流)
- 发送到指定消息队列中,供其他系统进行消费
更多接入细节可以参考其官网github.com/alibaba/can…
不过,像在一些比较大的公司,都会有基础架构的同学负责提供这样的数据传输服务(DTS),业务方负责订阅使用即可。
存储结构
对于Cache中存储的数据结构,只能存储与其数据表有关的数据,举个例子。
以商品数据为例,先简单介绍下商品的数据结构(后面有机会单独出一篇文章详细介绍~);
商品类目是指商品的分类,比如一级分类为数码产品,二级分类为手机;
商品SPU(Standard Product Unit)是指标准化产品单元,即以一个产品为一个单位。比如手机类目里面的Iphone13可以当作一个单位。
商品SKU(Stock Keeping Unit)是指库存量单元,库存一般会记录到更细致的粒度,比如Iphone13 远峰蓝色 256G,Iphone13 土豪金色 128G。颜色和内存被称作规格,所有规格的组合唯一确定商品的一个SKU。
CREATE TABLE `category` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT '类目名称',
...
) ENGINE=InnoDB ;
CREATE TABLE `product` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT '商品名称',
`category_id` int(11) NOT NULL COMMENT '商品类目',
`url` varchar(255) NOT NULL COMMENT '商品名称',
...
) ENGINE=InnoDB ;
CREATE TABLE `product_sku` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL COMMENT '商品类目',
`price` bigint(20) NOT NULL COMMENT '价格',
...
) ENGINE=InnoDB ;
查询SKU至少会有两个维度:
- 通过product_id查询该商品下所有的SKU。比如进入商品详情页时,列出所有可订购的选项;
- 通过sku_id查询唯一确定的一行记录。比如选择某一个SKU后进入订单确认页;
这两种维度存储到Redis中分别使用String和Hash类型存储;
- String类型:key是唯一键(sku_id),存储整个do对象;
- Hash类型:key是外键(product_id),field是唯一键(sku_id),存储的是整个do对象;
一般这两种维度是最常用的,可以考虑把这部分使用做一下封装,例如让业务方做一点配置就可以轻松实现缓存接入,例如配置Mybatis中的domain,example,mapper等信息,通过反射实现通用接入功能,而不需要自己去解析binlog消息的变更。
除了上面最常用的两种维度以外,还有很多特殊的维度,例如想要统计手机类目下所有在线的商品数量有多少,实现思路如下:
- 存储结构为String类型,key是外键(category_id),value是商品数量。
- 监听product表的insert,update,delete(推荐软删除)操作,insert时如果sale_status字段为上架状态,数量 + 1;update需要判断更新前 和 更新后sale_status的状态,如果由上架改为下架,数量 - 1,反之 + 1,一样则不变。delete时需要判断之前是否为上架状态,是则 - 1。
解决的问题
由于Cache数据作为了主要的数据源,所以也就不存在缓存击穿、雪崩和穿透问题了。
存在的问题
1.消息延迟
接收到消息平均可能会有大概几百毫秒的延迟,并且如果消息发送有问题,或者消息如果处理有问题会导致数据不一致的影响扩大。
2.消息乱序
由于消息消费一般都是无序的形式,所以需要进行数据的版本对比,一般会选择用更新时间戳或者单独的版本号,如果选择的是时间戳,只要收到的消息的更新时间要早于当前缓存中的更新时间,则表示是旧消息,直接忽略该消息即可。
3.消息丢失
我们都知道,消息是可能会丢失的,所以为了避免消息丢失带来数据不一致的影响,可以把全量同步task设定为定时任务,控制一下流量,保证不会由于一直没有更新操作导致数据一直不一致。
4.应用场景有限
由于会把数据表中的数据全部同步到缓存中,所以不适合数据量很大的业务,比如订单业务。但Redis一个单实例最多能存2的32次方的key,最少也能存2.5亿的key,所以集群场景下通常是能接受缓存key很多的,只是单个key不宜很大。
另外之前介绍了像类目下所有的商品数这种比较缓存方式,使用该模式来实现不一定简单。
最后
阅读完如果对您有帮助,记得点个赞和关注哦,有👍有动力
转载自:https://juejin.cn/post/7126544551304495117