JAVA 面试 Redis
文章目录
一、Redis数据结构
首先是五大基本数据结构string、hash、list、set、zset,以及高级数据结构hyperLogLog、geo、bitmap、stream等。
1、数据结构的作用
基本数据结构
- string:字符串,用来做缓存、计数器、分布式锁。
- hash:哈希表,可以用来存储对象。
- list:列表,元素存取有序,可以重复,可以用来实现队列、栈结构。
- set:集合,元素存取无序,不能重复,可以做交并差操作,例如计算两个人的共同标签。
- zset:有序结合,元素有序,不能重复,存放元素以及元素的得分,可以做时间窗口来限流,也可以做排行榜。
高级数据结构 - hyperLogLog:海量数据的统计,数据量越大,误差越小,比如可以做一些日PV,日UV。是用极小的内存统计非常大的体量,内存仅有12kb。
- geo:存储地址位置,经纬度,实现一些经纬度相关的查询。
- bitmap:位图,可以做布隆过滤器、连续登录天数等。
- stream:流,用于处理消息队列和事件流。
2、底层实现
- string:Redis没有使用C语言传统的字符串表示(以空字符结尾’\0’的字符串数组),而是自己构建了一个名为简单动态字符串(Simple Dynamic String, SDS)的抽象类型,并将SDS作为Redis的默认字符串表示。
- hash:ziplist+hash表。数据体量比较少的时候就用ziplist,默认条数超过512,或者内存超过64Kb,就会用hash表
- list:quicklist,本质就是ziplist用指针串连到一起。
- set:存储纯数值的时候是有序的数值数组,默认条数超过512,或者有字符串,就会用hash表。
- zset:ziplist+跳表。数据体量比较少的时候就用ziplist,默认条数超过128,或者内存超过64Kb,就会用hash表
- hyperLogLog:基于概率论中伯努利试验并结合了极大似然估算方法,并做了分桶优化。
- geo:本质就是用的zset
3、底层
3.1、ziplist:没有prev和next指针,但是却可以双向查询,且没有内存碎片化的问题。
- zlbytes:4字节,存储整个压缩表的字节数,最大2^32 - 1字节
- zltail:4字节,最后一个元素n的内存偏移量,从头开始,计算内存到元素n之前。
- zllen:2字节,元素个数,最多存储2^16 - 1个元素
- zlend:1字节,代表压缩链表结束
- 元素:
- prev_entry_length:前面元素的的长度,用于反向遍历,长度1字节/5字节
- encoding:元素类型,长度1字节/5字节
- content:元素内容


3.2、hyperLogLog
将一个long类型的hash分成两部分,低14位作为分桶索引,高50位用来计算桶内存放的值。先通过低14位找到对应的桶,然后在二进制表示下从左往右查看高50位,第n位置第一次出现1,就把n写入桶。由于n最大值是50,因此只需要6位来存储,因此所需内存只有 2^14 * 6 / 8 / 1024 = 12kb
二、数据库和缓存的一致性
关于如何保证数据库和缓存的一致性,这是一个在分布式系统中非常核心的问题。我的理解是,没有完美的银弹方案,核心是在性能、一致性和系统复杂度之间做一个权衡。我们通常追求的是最终一致性。
下面我由浅入深地阐述我的方案:
首先,最基础和常用的模式是 Cache-Aside (旁路缓存)。 它的读写作流程是:
读线程,先读缓存,缓存没有就会去数据库读取最新数据放到缓存;
写线程,写线程有两个方案,方案一是先更新数据库,再删除缓存,方案二是先删除缓存,再更新数据。
我比较推荐实用方案一,因为方案二有可能导致数据不一致问题,写线程A先删除缓存,然后读线程B进来发现没有缓存,就会去数据库读取缓存,把旧数据放到缓存,然后A再更新数据库数据,数据就不一致了,而且高并发下这种情况很容易出现。而方案一会有短暂的数据不一致情况,就是更新数据库后到删除缓存这段时间,对比来说还是方案一更好。但是极端情况下,还是可能会导致数据不一致,就是读线程A读缓存,发现没有缓存,然后再去数据库读数据,但是还没有写入缓存,这时写线程B进来把数据更新了,然后删除缓存,然后A把读到的旧数据写入到了缓存,导致数据不一致。这种情况极难出现,因为读线程A写缓存的操作很快,一般不足以让写线程B完成这么多操作后还没把数据写到缓存。但是还是有概率发生,这种情况下可以通过延迟双删的方式处理,就是在更新完数据后,发送一个mq消息或者起一个定时任务,在1s或者其他的时间后删除缓存。
然后还要考虑缓存删除失败的问题,添加重试机制,并且在一直失败的情况下,需要触发告警通知人工干预。
还可以使用canal监听binlog变化,当数据变化就删除缓存,而且将缓存删除操作与业务逻辑完全解耦。无论业务代码是如何处理请求的,只要数据库最终变了,缓存就会被删除,保证了最终一致性。这个方案的容错性非常好。
也可以通过设置缓存过期时间来处理,即使发生了不一致的问题,也会在缓存过期后加载到最新的数据,是一个非常简单而又有效的最终一致性兜底方案。
最后,我来总结一下我的最佳实践,对于大多数读多写少的业务场景,我会采用Cache-Aside + 先更新数据库,再删除缓存的策略,通过消息队列重试来保证删除操作的成功。在复杂或一致性要求高的系统中,引入 Canal监听Binlog 作为终极解决方案。然后尽量为所有缓存数据设置过期时间,做一个兜底方案。此外,对于一些极端高并发场景,我们可能还会讨论到‘延迟双删’等策略,但它引入延迟时间难以评估,通常不作为首选。
总之,我的方案是在保证高性能和低复杂度的前提下,通过技术组合拳来最大限度地保证数据的最终一致性。
更多推荐

所有评论(0)