MySQL与Redis 如何保证双写一致性

缓存延时双删
删除缓存重试机制
读取biglog异步删除缓存

缓存延时双删

redis双删的原理 redis异步双删_数据库


1、先删除缓存

2、再更新数据库

3、休眠一会(比如1秒),再次删除缓存。

这个休眠一会,一般多久呢?都是1秒?

这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。

这种方案还算可以,只有休眠那一会(比如就那1秒),可能有脏数据,一般业务也会接受的。但是如果第二次删除缓存失败呢?缓存和数据库的数据还是可能不一致,对吧?给Key设置一个自然的expire过期时间,让它自动过期怎样?那业务要接受过期时间内,数据的不一致咯?还是有其他更佳方案呢?

2 删除缓存重试机制

因为延时双删可能会存在第二步的删除缓存失败,导致的数据不一致问题。可以使用这个方案优化:删除失败就多删除几次呀,保证删除缓存成功就可以了呀~ 所以可以引入删除缓存重试机制

redis双删的原理 redis异步双删_数据库_02

写请求更新数据库

缓存因为某些原因,删除失败

把删除失败的key放到消息队列

消费消息队列的消息,获取要删除的key

重试删除缓存操作

3 读取biglog异步删除缓存

重试删除缓存机制还可以吧,就是会造成好多业务代码入侵。其实,还可以这样优化:通过数据库的binlog来异步淘汰key。

redis双删的原理 redis异步双删_数据库_03


可以使用阿里的canal将binlog日志采集发送到MQ队列里面

然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性

Redis的跳跃表

redis双删的原理 redis异步双删_redis双删的原理_04


跳跃表是有序集合zset的底层实现之一

跳跃表支持平均O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。

跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。

跳跃表就是在链表的基础上,增加多级索引提升查找效率。

为什么Redis 6.0 之后改多线程呢?

Redis6.0之前,Redis在处理客户端的请求时,包括读socket、解析、执行、写socket等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。
Redis6.0之前为什么一直不使用多线程?使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。
redis使用多线程并非是完全摒弃单线程,redis还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。

这样做的目的是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。

聊聊Redis 事务机制

Redis通过MULTI、EXEC、WATCH等一组命令集合,来实现事务机制。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

简言之,Redis事务就是顺序性、一次性、排他性的执行一个队列中的一系列命令。

Redis执行事务的流程如下:

开始事务(MULTI)
命令入队
执行事务(EXEC)、撤销事务(DISCARD )

命令 描述
EXEC 执行所有事务块内的命令
DISCARD 取消事务,放弃执行事务块内的所有命令
MULTI 标记一个事务块的开始
UNWATCH 取消 WATCH 命令对所有 key 的监视。
WATCH 监视key ,如果在事务执行之前,该key 被其他命令所改动,那么事务将被打断。

Redis的Hash 冲突怎么办

Redis 作为一个K-V的内存数据库,它使用用一张全局的哈希来保存所有的键值对。这张哈希表,有多个哈希桶组成,哈希桶中的entry元素保存了key和value指针,其中*key指向了实际的键,*value指向了实际的值。

redis双删的原理 redis异步双删_redis双删的原理_05


哈希表查找速率很快的,有点类似于Java中的HashMap,它让我们在O(1) 的时间复杂度快速找到键值对。首先通过key计算哈希值,找到对应的哈希桶位置,然后定位到entry,在entry找到对应的数据。

什么是哈希冲突?

哈希冲突:通过不同的key,计算出一样的哈希值,导致落在同一个哈希桶中。

Redis为了解决哈希冲突,采用了链式哈希。链式哈希是指同一个哈希桶中,多个元素用一个链表来保存,它们之间依次用指针连接。

redis双删的原理 redis异步双删_缓存_06


有些读者可能还会有疑问:哈希冲突链上的元素只能通过指针逐一查找再操作。当往哈希表插入数据很多,冲突也会越多,冲突链表就会越长,那查询效率就会降低了。

为了保持高效,Redis 会对哈希表做rehash操作,也就是增加哈希桶,减少冲突。为了rehash更高效,Redis还默认使用了两个全局哈希表,一个用于当前使用,称为主哈希表,一个用于扩容,称为备用哈希表。

在生成 RDB期间,Redis 可以同时处理写请求么?

可以的,Redis提供两个指令生成RDB,分别是save和bgsave。

如果是save指令,会阻塞,因为是主线程执行的。
如果是bgsave指令,是fork一个子进程来写入RDB文件的,快照持久化完全交给子进程来处理,父进程则可以继续处理客户端的请求。

Redis底层,使用的什么协议?

RESP,英文全称是Redis Serialization Protocol,它是专门为redis设计的一套序列化协议. 这个协议其实在redis的1.2版本时就已经出现了,但是到了redis2.0才最终成为redis通讯协议的标准。

RESP主要有实现简单、解析速度快、可读性好等优点。

布隆过滤器

应对缓存穿透问题,我们可以使用布隆过滤器。布隆过滤器是什么呢?
布隆过滤器是一种占用空间很小的数据结构,它由一个很长的二进制向量和一组Hash映射函数组成,它用于检索一个元素是否在一个集合中,空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

**布隆过滤器原理是?**假设我们有个集合A,A中有n个元素。利用k个哈希散列函数,将A中的每个元素映射到一个长度为a位的数组B中的不同位置上,这些位置上的二进制数均设置为1。如果待检查的元素,经过这k个哈希散列函数的映射后,发现其k个位置上的二进制数全部为1,这个元素很可能属于集合A,反之,一定不属于集合A。

来看个简单例子吧,假设集合A有3个元素,分别为{d1,d2,d3}。有1个哈希函数,为Hash1。现在将A的每个元素映射到长度为16位数组B。

redis双删的原理 redis异步双删_数据库_07


我们现在把d1映射过来,假设Hash1(d1)= 2,我们就把数组B中,下标为2的格子改成1,如下:

redis双删的原理 redis异步双删_缓存_08


们现在把d2也映射过来,假设Hash1(d2)= 5,我们把数组B中,下标为5的格子也改成1,如下

redis双删的原理 redis异步双删_redis_09

接着我们把d3也映射过来,假设Hash1(d3)也等于 2,它也是把下标为2的格子标1:

redis双删的原理 redis异步双删_数据库_10


因此,我们要确认一个元素dn是否在集合A里,我们只要算出Hash1(dn)得到的索引下标,只要是0,那就表示这个元素不在集合A,如果索引下标是1呢?那该元素可能是A中的某一个元素。因为你看,d1和d3得到的下标值,都可能是1,还可能是其他别的数映射的,布隆过滤器是存在这个缺点的:会存在hash碰撞导致的假阳性,判断存在误差。

如何减少这种误差呢?

搞多几个哈希函数映射,降低哈希碰撞的概率
同时增加B数组的bit长度,可以增大hash函数生成的数据的范围,也可以降低哈希碰撞的概率
我们又增加一个Hash2哈希映射函数,假设Hash2(d1)=6,Hash2(d3)=8,它俩不就不冲突了嘛,如下:
我们又增加一个Hash2哈希映射函数,假设Hash2(d1)=6,Hash2(d3)=8,它俩不就不冲突了嘛,如下:

图片

redis双删的原理 redis异步双删_数据库_11


即使存在误差,我们可以发现,布隆过滤器并没有存放完整的数据,它只是运用一系列哈希映射函数计算出位置,然后填充二进制向量。如果数量很大的话,布隆过滤器通过极少的错误率,换取了存储空间的极大节省,还是挺划算的。

目前布隆过滤器已经有相应实现的开源类库啦,如Google的Guava类库,Twitter的 Algebird 类库,信手拈来即可,或者基于Redis自带的Bitmaps自行实现设计也是可以的。