boolean locked = lock.tryLock(key, 10, TimeUnit.SECONDS);
if (locked) {
} else {
throw new RuntimeException("请勿重复提交");
热点数据缓存
在优惠券系统中,有一个使用热点缓存非常经典的场景,那就是券模板信息,券模板信息在发券,咨询券,核券,回退券等核心链路中发挥了非常关键的作用,像大批量发券场景就会查询某些券模板信息,比如券门槛,券面额等信息。如果每次发券都查询数据库,那数据库一定会存在非常的压力,这样的肯定是非常不合理的。在我们优惠券系统中,通过caffeine+redis实现了多级缓存,缓解数据库的压力。
在优惠券的后台管理系统中,一般有某个券模板发放量,使用量的统计展示,在我们优惠券系统中这个功能就是通过redis自增计数器功能来实现的。
redis在项目中使用的场景还是非常多的,我们只要结合自己的项目回答即可。
二、Redis有效期怎么设置
这个问题就是考察redis的最佳实践了,在实际项目中,我们一般有两个比较重要的准则:
1、尽量给每个key都设置有效期,避免不需要的key过多,占用内存资源
2、将同一个类型的redis key的有效期打散,避免批量缓存数据失效,导致穿透redis的问题,比如第一点里面,缓存券模板的过期时间就是不同随机因子打散的。
三、redission实现分布式锁的原理
这个题目需要对redission的源码有过阅读才能更好的回答,redission的源码阅读起来并不难,有时间还是可以去看一看的。
我们看看一下redission加锁后在redis中是怎么存储的?
没错,redission中分布式锁使用的是hash结构来存储的。看到这样就可以和面试官说redission底层使用了redis的hash结构来进行存储分布式锁信息。
hash key就是我们设置的锁key: dmb
hash 属性key xxxxx:85 xxxx是uuid, 85代表加锁成功的线程
hash value 2 代表加锁成功,重入了2次。
我们可以和面试官说我阅读过加锁解锁的源码:
上面这段代码是加锁的核心实现
,底层通过lua脚本控制加锁逻辑,lua脚本是redis官方支持原子性执行多个redis命令的机制。整个加锁流程非常清晰简短,就是判断锁的key是否存在,不存在则加锁,否则判断锁是否属于当前线程的,是则加锁次数自增1,不是则返回锁的剩余时间。
接下来就是解锁的源码:
通过解锁源码看出,解锁会判断锁是不是被当前线程持有,如果是需要判断锁次数是否大于1,大于1需要加锁次数减1,如果减到了0则需要删除key,这样就释放锁成功了。
还有一个重要的知识点就是锁自动续期,也就是大家常说的看门狗设计。这个是在加锁核心逻辑的上层实现的。
看门狗的实现是通过netty的时间轮组件来实现定时续期功能的,定时任务里每隔3分之加锁时间就会尝试续期一次。
redis经典问题
一、redis缓存如何和数据库保持一致性?
这个问题我也记得被问过很多次,这个一般要具体的场景具体分析,如果是没有并发的业务场景下,我们很容易实现缓存和数据库的一致性,但是在高并发场景下就很难了,所以我们需要根据自身实际业务的场景选择最合适的方案,在我的实际项目中主要的场景都是使用的最终一致性的方案,比如上方说到的优惠券模板缓存信息,实际上就是满足最终一致性的。
回答这个问题,我们可以和面试官先讲下业界比较认可的方案,再举例讲下实际业务使用的方案。
保证最终一致性Cache Design Pattern方案
缓存模式其实是计算机体系操作系统层面的内容,是解决cpu和内存数据一致性的方案。缓存模式有四种:
缓存模式 | 更新缓存设计 |
---|
Cache aside Pattern | 更新数据库时删除缓存,查询数据库加载缓存 |
Read Through Pattern | 更新数据库时不直接更新缓存,读数据时更新缓存 |
Write Through Pattern | 更新数据库时更新缓存 |
Write Behind Caching Pattern | 先同步更新缓存,再异步更新数据库 |
我讲下最常用的缓存模式,他是在更新缓存后删除缓存,在查询操作未命中缓存的时候再更新缓存。我们系统中也使用的是这种方案。
这个方案并不会完全保证更新缓存之后缓存数据一致,有极低的概率存在不一致,比如线程1查询缓存未命中后查询数据库,这个时候来了一个线程2更新数据库并删除缓存操作,这个时候线程1查到的旧数据就会写到缓存了,这样的弊端就是需要等候下次缓存时间到了才会更新为最新的数据。
为了解决上面的问题,在业界推出了缓存双删的方案。
这个方案可以最大程度避免上面提到的问题,延迟时间的设置需要考虑业务场景。
强一致性方案
如果真的要求强一致方案,数据库更新的同时,redis也更新,那么我们就需要采用分布式事务了,比如二阶段提交,但是这样做的后果就是性能急剧下降,这就得看业务实时性重要还是性能以及用户体验重要了。在一般情况下,我们通常会和业务说明,选择最终一致性方案。
这个题目真的要结合自己的业务场景回答,或者结合面试官给的业务场景回答,不同的业务场景对数据一致性的要求也不一样。
二、讲一讲缓存穿透、缓存击穿、缓存雪崩
缓存穿透是指查询一些数据库中压根不存在的数据,导致每次查询都穿透到数据库了,这种也有很多成熟的方案解决,比如使用布隆过滤器将数据库存在的数据存储一份,每次查询缓存前都查询布隆过滤器是否存在,存在则执行后续查询逻辑,否则直接返回。
缓存击穿是指热点key失效,多个线程同时请求数据库,这个问题通常使用jvm锁和分布式锁就可以避免拦截大部分请求,如果是使用本机缓存比如caffiene的情况,它底层就是使用synchronized
加锁使单机最多一个线程穿透到数据库。而如果是加载分布式缓存如redis我们需要使用分布式锁来控制只要一个进程可以操作数据库了。
拿加载redis来说:
缓存雪崩是指大量缓存key同时失效,导致批量请求打到数据库,这个时候db压力大,可能导致数据库扛不住,最终可能拖垮应用。这个问题通常都会将缓存设置的有效期打散,避免同时失效,或者通过定时任务提前预热缓存。
三、redis大key、热key如何发现解决?
这个问题其实挺好的,我们日常开发和巡检中关注比较多的点,可以考察面试者有没有实际解决热key和大key的生产问题。大key和热key对redis服务器的稳定性有很大的挑战,我们应该尽量避免大key的产生。
如果我们使用的云上的redis,比如腾讯云,他们提供了大key和热key监控,可以在发现热key的时候告警通知我们开发或者运维人员。
腾讯云官方对热key和大key有定义,以及有成熟的解决方案
www.tencentcloud.com/zh/document…
如果没有使用腾讯云等云上的redis,我们有哪些手段可以分析热key和大key呢?
大key发现手段
在redis 4.0之后可以通过redis-cli --big-keys
命令分析每种类型下最大的key。
Redis自4.0起提供了MEMORY USAGE命令来帮助分析Key的内存占用
我们还可以通过redis的慢操作日志分析,查看操作key的耗时,可通过CONFIG SET slowlog-log-slower-than 100
,控制大于多长时间才算慢。可以通过slowlog get
命令查看慢日志。
使用redis-rdb-tools工具以定制化方式找出大Key
大key解决手段
及时清理无效的数据,注意大key使用延迟删除(异步删除),unlink
命令
压缩大value
对大key拆分,对多个key查询可以通过用mget批量查询
增强监控,对redis内存增长加强监控,及时发现减少大key的影响
热key发现手段
我们可以在应用客户端记录监控每个key的访问次数
Redis自4.0起提供了hotkeys参数来方便用户进行实例级的热Key分析功,该参数能够返回所有Key的被访问次数
Redis的monitor命令能够打印Redis中的所有请求,包括时间信息、Client信息、命令以及Key信息。
热key解决手段
在架构上选择读写分离,分离读写压力
热key缓存在本地缓存,避免请求redis
阿里的Tair缓存代理框架能智能识别热key,并缓存在代理层,不会查询redis。
基础数据结构
一、讲下redis string底层实现的原理
这个题目需要对string类型的实现有所了解,否则不好开头回答,在string类型中,通过简单动态字符串SDS(simple Dynamic String)存储类型来实现。
我们可以脑海里回忆下SDS的结构体定义:
sds结构体其实包含装了c中的char数组结构,包含了数据长度和char数组,以及容量,一定定义了5种长度sds结构,redis存储stringl类型数据时会根据字符串长度决定使用哪个结构体存储。
从sds的结构定义可以看出sds比传统的char数组有以下优势:
根据len属性直接判断字符串长度,时间复杂度O(1),char数组O(n)
有不同长度的定义,可以动态扩容
c中的char数组用\0
作为结束符,不适合存储二进制数据,而sds不需要根据\0
判断字符串是否结束,可以根据len属性判断
在redis中,不同的数据结构需要说原理的话,主要是插入,删除,更新,扩容,缩容等操作的流程机制,string数据结构的功能也无非是这几个。
上面就是string类型数据的插入和更新的主流程,值得一说的就是他的内存空间扩容策略和惰性空间释放策略
内存空间扩容机制
也就是在数据大小是1M之前扩容原空间2倍,在1M之后每次增加1M内存空间。
惰性空间释放
惰性空间释放用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来,并等待将来使用。
void sdsclear(sds s) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
sh->free += sh->len;
sh->len = 0;
sh->buf[0] = '\0';
二、讲讲zset的底层数据结构
zset是一个有自动排序能力的数据结构。
每个元素有score,默认按score从小到大排序,score相同时,按照key的ascii码排序
。
redis底层使用了两种结构ziplist和skiplist来表示zset。根据zset的member数量选择不同的结构:
从上面可知,zset其实底层有两种数据结构支撑,skiplist只是其中一种结构,而面试官一般都喜欢问跳表结构。
跳表是由链表组成的,链表里的元素是一个数组结构,我们可以看一下redis中跳表的定义,比较清晰:
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
跳表结构比较好理解,就是经典的用空间换时间设计思想,将数据冗余在多层,最底层数据是最完整的,越往上层数据越少,这样设计就是为了查找快速。
每次插入数据都会从最上层开始查找插入位置。选择好了插入位置后会随机生成一个层级,根据是否是新层级构建跳表节点。
下面图可以表示redis中的跳表结构:
redis跳表的特点
用c语言翻译实现了William Pugh
的平衡树的概率替代方案。
支持重复的score成员
支持分数排序,也支持数据排序
有一个后向指针,所以它是一个双向链表(回忆下B+树的叶子节点,是不是很相似?),后向指针仅在“级别 1”。这允许从尾到头遍历列表,对ZREVRANGE很有用
redis高可用方案
一、说一说redis的主从集群
虽然redis性能高,由于还是会存在单机故障,因此redis还是需要主从集群,从节点需要和主节点数据同步,这样既可以实现故障切换,还可以实现读写分离,提高redis可用性。
我之前的文章有详细分析过主从复制的原理,有需要的朋友可以去看看。如果是面试的时候可以和面试官直接说下面的重点。
redis主从集群主要涉及到主从复制的功能。复制数据又分为全量复制和增量复制。
在redis官方也有解释说明:redis.io/docs/manage…
全量复制:
master启动一个后台save进程去生产rdb文件,同时启动创建缓存用于缓存客户端发送的写命令,如果生产rdb文件完成,将rdb文件发送到slave,slave将rdb文件保存到磁盘,然后加载到内存。然后master将所有缓存的命令(和redis本身协议一致)发送到副本。
小结一下:全量同步:总体有两个数据,一个是rdb快照文件,一个生成快照过程中的客户端命令缓存。
增量复制:
增量复制过程是通过psync命令完成的,从节点会将复制id,上次同步的最大偏移量发送到主节点,主节点根据偏移量生成rdb文件,最后发送给从节点。
二、请你聊一聊哨兵模式sentinel
有了主从复制,为什么还需要哨兵模式呢?因为主从模式有他的短板,主从模式在节点故障的时候没法自动将从节点切换到主节点,而且客户端也无法感知新的主节点。这个时候哨兵模式就出现了,哨兵模式是给redis主从集群的节点增加了监控的节点,如果master出现故障,sentinel可以实现故障转移,并且通知客户端连接新的master。
哨兵部署架构:每个哨兵都会监听redis集群里的每个节点。
sentinel节点节点之间是通过redis的pub/sub机制实现通信的,这样可以指定哨兵有哪些节点,他们之间不会直接创建连接。
如果redis的master出现问题,那么sentinel会开始senntinel选举和redis的选举过程。
master故障需要sentinel集群确认master故障。包含主观下线和客观下线。
主观下线是sentinel单个节点发现master超过了指定时间没有回复ping命令。
客观下线是指有超过配置的法定哨兵数认为master主观下线后,确认master为客观下线。
sentine确认master主观下线后,会执行故障转移。首先会选择一个主sentinel完成故障转移。
先和面试官说重点:通过sentinel选举领导者。使用raft算法(状态共识算法)
每一个Sentinel节点都可以成为Leader,当一个Sentinel节点确认redis集群的主节点主观下线后,会请求其他Sentinel节点要求将自己选举为Leader。被请求的Sentinel节点如果没有同意过其他Sentinel节点的选举请求,则同意该请求(选举票数+1),否则不同意。
如果一个Sentinel节点获得的选举票数达到Leader最低票数(quorum和Sentinel节点数/2+1的最大值),则该Sentinel节点选举为Leader;否则重新进行选举。
Raft核心思想:先到先得,少数服从多数。
选举好主sentinel后,就是选择最新的redis master了。
选择master时候会考虑从节点(副本)下面几个情况:
与主机断开连接的时间。过滤发现与master服务器断开连接的时间超过配置的master超时时间down-after-milliseconds的副本slaves
副本优先级。 优先选择replica-priority低的。
如果优先级相同,已处理复制偏移量。越大越优先,这个更符合业务场景功能。
如果复制offset相同,就看运行ID。优先选择小的。
选择好master节点后,维护新集群
sentinel节点,向新主节点发送,slave no one命令,让它成为独立节点
sentinel节点,向其他从节点发送 slaveof ip port,跟随到主节点
到这里哨兵机制的原理就说清楚了。内容挺多,如果没有准备好,要讲清楚确实挺难。
三、请你聊一聊Cluster集群
为什么有了哨兵还需要Redis的Cluster集群方案呢?
哈哈哈,这个问题就是随着前面两个问题来的啊,如果理解了主从集群和哨兵,我们知道他们还是存在短板,主从集群没法主动选主master,哨兵没有做数据分片,这样redis单机性能瓶颈还是存在。如果需要提高redis的横向扩展能力,还想需要实现分片能力。Redis的Cluster方案就是redis官方提供的redis分片高可用架构。我现在的公司大部分核心业务也使用的是Cluster集群方案,这样可以把c端用户的读缓存压力分担到多个redis分片上。
Redis Cluster是使用虚拟槽实现的。总共创建16384个槽,每个redis节点负责一个区间段的虚拟槽。
master节点根据bitmap来保存slot关系,0代表不属于本节点,1代表属于当前节点。
为什么是16384个槽呢?为什么不是65535?
因为Redis每秒都需要发送心跳包,槽位信息也需要发送,如果槽位为65536,发送心跳信息的消息头达65536÷8÷1024=8kb
,发送的心跳包过于庞大,因此16384够用。
redis不建议超过1000个节点,如果节点过1000个,也会导致网络拥堵。16384
Redis master节点的配置信息中,它所负责的哈希槽是通过一张bitmap
的形式来保存的,槽位数越小,压缩率越高
计算key属于哪个槽是通过CRC16算法以及取模得到的:
HASH_SLOT = CRC16(key) mod 16384
在redission客户端,客户端初始化和定时任务会从master拉取slot信息,并进行本地缓存。并且每次操作key都会计算slot。
if (key.hasArray()) {
end = CRC16.crc16(key.array(), key.position(), key.limit() - key.position()) % 16384;
return end;
end = CRC16.crc16(key) % 16384;
Redis持久化机制
redis有两种持久化机制,面试官一般都是问两者的区别,以及各自的优劣势。
rdb是定时生成快照文件,而aof是追加命令的机制。
一、RDB
RDB工作原理
通过fork一个子进程进行生成rdb文件,然后将老的rdb文件替换。
RDB特点
rdb可以在指定时间和命令数之后生成快照文件,比如save 60 1000
代表每60秒有1000个,如果发生断电,最近修改的数据会丢失。
他的大小比aof小,恢复速度比aof快。
二、AOF
AOF工作原理
和rdb一样,aof也会fork一个子进程,子进程通过写时复制机制,生成一个临时的aof文件,进行重写aof文件,同时把重写期间的操作的命令写入一个缓冲buffer区,等重写完后把buffer区的内容同步到aof文件,最后将临时文件替换成旧的aof文件。
AOF特点
aof通过不断追加命令到文件完成持久化,
支持三种刷盘机制:每秒/每次写完新命令/不主动刷盘(依赖操作系统自己刷盘),丢失数据概率低。
aof支持重写功能,但是它比rdb的文件大小会大一些,恢复速度比rdb要慢。
这波Redis的复盘到此就结束了,感觉每个题目都被问到过。
服务端技术栈
公众号: 服务端技术栈 | 源码研究工程师
粉丝