RDB是Redis用来进行持久化的一种方式,是把当前内存中的数据集快照写入磁盘,也就是 Snapshot 快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存里。
RDB 有两种触发方式,分别是自动触发和手动触发
在配置文件中设置:save m n,表示m秒内数据集存在n次修改时,自动触发bgsave(这个命令下面会介绍,手动触发RDB持久化的命令)
1、save
该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。
显然该命令对于内存比较大的实例会造成长时间阻塞,这是致命的缺陷,为了解决此问题,Redis提供了第二种方式。
2、bgsave
执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。
基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。
RDB 适合做冷备的原因如下:
RDB 文件生成后,改变的频率低,除非频繁触发检查点导致重新生成。
RDB 是 Redis 内存快照,比 AOF 日志恢复速度快。
RDB 的生成策略可以自行配置,而且可以配置多项,可以根据系统的使用场景和实际情况进行设置。
1.用 Linux 自带的 crontab 命令执行定时任务,调用数据备份脚本。
2.每小时备份一份一次当前最新的 RDB 快照文件到指定目录,只保留最近 48 小时的备份。
3.每天备份一份当前最新的 RDB 快照文件到指定目录,只保留最近一个月的 备份。
4.每天晚上将备份文件都发送远程的云服务器上。
①、优势
1.RDB是一个非常紧凑(compact)的文件,它保存了redis 在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。
2.生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
3.RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
②、劣势
1、RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,如果不采用压缩算法(内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑),频繁执行成本过高(影响性能)
2、RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题(版本不兼容)
3、在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改(数据有丢失)
同步命令到 AOF 文件的整个过程可以分为三个阶段:
1.进行命令传播
2.将我的命令传播改为redis的编程方式
3.追加到AOF缓存末尾处
修改配置文件appendonly yes
AOF的重写
好处:数据齐全,且数据一致性好,坏处:回复慢
停止 Redis,暂时关闭 AOF 的持久化配置。
删除 AOF 日志文件和 RDB 快照文件。
拷贝 RDB 快照文件到 Redis 的 RDB 文件加载目录。
重启 Redis,确认数据恢复成功。
热修改 Redis 的 AOF 持久化配置,Redis 会将内存中的数据写入到 AOF 文件中。
再次停止 Redis,手动修改配置文件,打开 AOF 持久化,防止热修改不生效。
再次重启 Redis。
从节点发送命令主节点做bgsave同时开启buffer
redis同步,第一次主节点会做一个bgsave,将之后期间产生的数据存在内存buffer中,待bgsave后将rdb文件全量复制到从节点,从节点再接受完全量文件会加载到内存中,加载内存完后再通知主节点将期间数据同步到从节点,进行重放即可完成。
也可以不然主服务器进行磁盘保存,只让其中一个从服务器进行磁盘保存,但是可能会造成,当主服务器宕机了,突然又重启,这时rdb文件为空,那么传播给从服务器,从服务器就也变为空了
全量复制(也称为快照同步)和增量复制
监控:哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。
提醒:当被监控的某个Redis节点出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。
自动故障转移:当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作。
何为脑裂?当一个集群中的 master 恰好网络故障,导致与 sentinal 通信不上了,sentinal会认为master下线,且sentinal选举出一个slave 作为新的 master,此时就存在两个 master了。
此时,可能存在client还没来得及切换到新的master,还继续写向旧master的数据,当master再次恢复的时候,会被作为一个slave挂到新的master 上去,自己的数据将会清空,重新从新的master 复制数据,这样就会导致数据缺失。
数据丢失解决方案
如上两个配置:要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒,如果超过 1 个 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。
不仅提供了高可用的手段,同时数据是分片保存在各个节点中的,可以支持高并发的写入与读取。当然实现也是其中最复杂的。
该模式就支持动态扩容,可以在线增加或删除节点,而且客户端可以连接任何一个主节点进行读写,不过此时的从节点仅仅只是备份的作用。至于为何能做到动态扩容,主要是因为Redis集群没有使用一致性hash,而是使用的哈希槽。Redis集群会有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,而集群的每个节点负责一部分hash槽。
那么这样就很容易添加或者删除节点, 比如如果我想新添加个新节点, 我只需要从已有的节点中的部分槽到过来;如果我想移除某个节点,就只需要将该节点的槽移到其它节点上,然后将没有任何槽的A节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。
这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。
为Cluster动态增加节点(也就是添加的时候不影响正在运行的节点), 需要先主后从, slot迁移在添加主节点之后; 反之, 动态删除节点, 需要先从后主, slot迁移在删除主节点之前
数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。
使过期时间分布不均匀,特别是热点的key
集群部署保证高可用
如根据 key 去缓存层查询数据,当缓存层为命中时,对 key 加锁,然后从存储层查询数据,将数据写入缓存层,最后释放锁。假设在高并发下,缓存重建期间 key 是锁着的,如果当前并发 1000 个请求,其中 999 个都在阻塞,会导致 999 个用户请求阻塞而等待。
如在Redis 中将 key 的过期时间设置为 60 min,在对应的 value 中设置逻辑过期时间为 30 min。这样当 key 到了 30 min 的逻辑过期时间,就可以异步更新这个 key 的缓存,但是在更新缓存的这段时间内,旧的缓存依然可用。这种异步重建缓存的方式可以有效避免大量的 key 同时失效。
设置逻辑过期时间
某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。
setnx,锁jvm
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
1.布隆过滤,2. 缓存空对象. 将 null 变成一个值.3.用户校验,看用户是否合法
将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟
1、读:
(1)先读cache,如果数据命中则返回
(2)如果数据未命中则读db
(3)将db中读取出来的数据入缓存
2、写:
(1)先淘汰cache
(2)再写db
先操作缓存,在写数据库成功之前,如果有读请求发生,可能导致旧数据入缓存,引发数据不一致。
在分布式环境下,数据的读写都是并发的,上游有多个应用,通过一个服务的多个部署(为了保证可用性,一定是部署多份的),对同一个数据进行读写,在数据库层面并发的读写并不能保证完成顺序,也就是说后发出的读请求很可能先完成(读出脏数据)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BK4SkzKe-1640005041824)(https://www.pianshen.com/images/760/de4db020093f40a67eab9b54ea26e7e8.png)]
上图解析: 写操作先执行1,删除缓存,再执行2,更新db;而读操作先执行3,读取cache数据,未找到数据时执行4,查询db。
问题所在: 写操作2没执行完时,读操作4执行了,则读到了脏数据到cache中,造成了cache和db的数据不一致问题。
方案1:Redis设置key的过期时间。
方案2:采用延时双删策略。
(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠1秒,再次淘汰缓存
这么做,可以将1秒内所造成的缓存脏数据,再次删除。(为什么是1秒?须要评估本身的项目的读数据业务逻辑的耗时。这么作的目的,就是确保读请求结束,写请求能够删除读请求形成的缓存脏数据。固然这种策略还要考虑redis和数据库主从同步的耗时。)
需要注意的是,如果你的 redis 使用了数据分片的方式,那么这个方法就不适用了。
适合分布式环境,不用关心 redis 是否为分片集群模式。
在业务层进行控制,操作 redis 之前,先去申请一个分布式锁,拿到锁的才能操作。
分布式锁的实现方式很多,比如 ZooKeeper、Redis 等。
适合有序场景
1.对象需要每次都整存整取
可以尝试将对象分拆成几个key-value, 使用multiGet获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响;
1、单个简单的key存储的value很大
(1)对象需要每次都整存整取
可以尝试将对象分拆成几个key-value, 使用multiGet获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响;
(2)该对象每次只需要存取部分数据
可以像第一种做法一样,分拆成几个key-value, 也可以将这个存储在一个hash中,每个field代表一个具体的属性,使用hget,hmget来获取部分的value,使用hset,hmset来更新部分属性
2、 hash, set,zset,list 中存储过多的元素
可以对存储元素按一定规则进行分类,分散存储到多个redis实例中。
对于一些榜单类的场景,用户一般只会访问前几百及后几百条数据,可以只缓存前几百条以及后几百条,即对用户经常访问的数据做缓存(正序倒序的前几页),而不是全部都做,对于获取中间的数据则可以直接从数据库获取
3、一个集群存储了上亿的key
如果key的个数过多会带来更多的内存空间占用,
1.key本身的占用。
2.集群模式中,服务端有时需要建立一些slot2key的映射关系,这其中的指针占用在key多的情况下也是浪费巨大空间。
所以减少key的个数可以减少内存消耗,可以参考的方案是转Hash结构存储,即原先是直接使用Redis String 的结构存储,现在将多个key存储在一个Hash结构中
对缓存操作的改善可以利用pipeline管道
拆分之后可以考虑采用pipeline去取,由于redis是单线程的,一次只能执行一个命令,这里采用Pipeline模式,一次发送多个命令,无需等待服务端返回。这样就大大的减少了网络往返时间,提高了系统性能。
pipeline:这个过程称为Round trip time(简称RTT, 往返时间),mget mset有效节约了RTT,但大部分命令(如hgetall,并没有mhgetall)不支持批量操作,需要消耗N次RTT ,这个时候需要pipeline来解决这个问题。
1、未使用pipeline执行N条命令
2、使用了pipeline执行N条命令
这个方案也很简单。不要让key走到同一台redis上不就行了。我们把这个key,在多个redis上都存一份不就好了。接下来,有热key请求进来的时候,我们就在有备份的redis上随机选取一台,进行访问取值,返回数据。
定时删除:需要另外创建一个定时器,消耗内存
定期删除:定期检查一部分删除,删除为随机
惰性删除:会导致大量不用的key堆积,造成内存泄漏,删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步。
redis采用的定期删除和惰性删除
过期key对RDB没有任何影响
过期key对AOF没有任何影响
redis淘汰机制的存在是为了更好的使用内存,用一定的缓存丢失来换取内存的使用效率。
手撕 LRU算法(最近最久未使用算法)
//LRU淘汰机制算法,要求手写
//① LinkedHashMap实现
import java.util.Map;
import java.util.LinkedHashMap;
public class LRU<K,V> {
private static final float hashLoadFactory = 0.75f;
private LinkedHashMap<K,V> map;
private int cacheSize;
public static void main(String[] args) {
LRU<String, String> lru = new LRU<String, String>(5);
lru.put("1", "1");
lru.put("2", "2");
lru.put("3", "3");
lru.put("4", "4");
lru.put("5", "5");
lru.print();
lru.put("6", "6");
lru.print();
lru.get("3");
lru.print();
lru.put("7", "7");
lru.print();
lru.get("5");
lru.print();
}
public LRU(int cacheSize) {
this.cacheSize = cacheSize;
//ceil, 向上取整; floor 向下取整
//计算map容量
int capacity = (int)Math.ceil(cacheSize / hashLoadFactory) + 1;
System.out.println(capacity);
map = new LinkedHashMap<K,V>(capacity, hashLoadFactory, true){
private static final long serialVersionUID = 1;
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
//map元素个数 > cacheSize时返回true, 移除元素
return size() > LRU.this.cacheSize;
}
};
}
public synchronized V get(K key) {
return map.get(key);
}
public synchronized void put(K key, V value) {
map.put(key, value);
}
public synchronized void clear() {
map.clear();
}
public synchronized int usedSize() {
return map.size();
}
public void print() {
for (Map.Entry<K, V> entry : map.entrySet()) {
System.out.print("key:" + entry.getKey() + ", value:" + entry.getValue() + ". ");
}
System.out.println("");
}
}
比如我们需要在10秒内限定20个请求,那么我们在setnx的时候可以设置过期时间10,当请求的setnx数量达到20时候即达到了限流效果。代码比较简单就不做展示了。
缺点:当然这种做法的弊端是很多的,比如当统计1-10秒的时候,无法统计2-11秒之内,如果需要统计N秒内的M个请求,那么我们的Redis中需要保持N个key等等问题
基于redis数据结构的zset操作
//zset实现滑动窗口
public Response limitFlow(){
Long currentTime = new Date().getTime();
System.out.println(currentTime);
if(redisTemplate.hasKey("limit")) {
Integer count = redisTemplate.opsForZSet().rangeByScore("limit", currentTime - intervalTime, currentTime).size(); // intervalTime是限流的时间
System.out.println(count);
if (count != null && count > 5) {
return Response.ok("每分钟最多只能访问5次");
}
}
redisTemplate.opsForZSet().add("limit",UUID.randomUUID().toString(),currentTime);
return Response.ok("访问成功");
}
通过上述代码可以做到滑动窗口的效果,并且能保证每N秒内至多M个请求,缺点就是zset的数据结构会越来越大。实现方式相对也是比较简单的。
令牌桶算法提及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了
控制输入
// 输出令牌
public Response limitFlow2(Long id){
Object result = redisTemplate.opsForList().leftPop("limit_list");
if(result == null){
return Response.ok("当前令牌桶中无令牌");
}
return Response.ok(articleDescription2);
}
// 10S的速率往令牌桶中添加UUID,只为保证唯一性
@Scheduled(fixedDelay = 10_000,initialDelay = 0)
public void setIntervalTimeTask(){
redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
}
控制输出
make_space 灌水之前调用漏水 腾出空间 取决于流水速率
HASH原子性有问题
SSCAN 游标 数目
单线程redis快的原因
1.纯内存操作
2.单线程操作
3.采用非阻塞I/O多路复用
这里主要来说一下redis的非阻塞I/O多路复用的实现
客户端连接到服务器会有许多个,他们会执行很多不同的命令,如应答,写入,读取,关闭等操作,因此会存在一个服务器连接了多个套接字且这些事件并发出现的场景。
所有套接字统一进入redis的多路复用程序,多路复用程序将这些套接字放入一个套接字队列,然后有序,同步,每次一个的将套接字传给文件事件分派器。
分派器接收到套接字,并根据套接字产生时间的类型,调用相应的事件处理器。
事件处理器就是一个个函数,他们定义了某个事件发生时候服务器应该执行的操作。
其中evport,epoll,kqueue复用函数的选择都是时间复杂的O(1),使用了内核内部的结构,并且能够服务几十万的文件描述符,而select是O(n),且最多同时服务1024个文件描述符。
非I/O多路复用的缺点1. read得读到很多才返回 为0会卡在那 直到新数据来或者链接关闭
2.写不会阻塞除非缓冲区满了
非阻塞的IO 提供了一个选项 no_blocking 读写都不会阻塞 读多少写多少 取决于内核的套接字字节分配
非阻塞IO也有问题 线程要读数据 读了一点就返回了 线程什么时候知道继续读?写一样
一般都是select解决 但是性能低 现在都是epoll
select 可以监控 FD 的可读、可写以及出现错误的情况。
严格来说,Redis Server是多线程的,只是它的请求处理整个流程是单线程处理的
与分布式锁相对应的是「单机锁」,我们在写多线程程序时,避免同时操作一个共享变量产生数据问题,通常会使用一把锁来「互斥」,以保证共享变量的正确性,其使用范围是在「同一个进程」中。
如果换做是多个进程,需要同时操作一个共享资源,如何互斥呢?
SETNX 执行成功,执行 EXPIRE 时由于网络问题,执行失败
SETNX 执行成功,Redis 异常宕机,EXPIRE 没有机会执行
SETNX 执行成功,客户端异常崩溃,EXPIRE 也没有机会执行
总之,这两条命令不能保证是原子操作(一起成功),就有潜在的风险导致过期时间设置失败,依旧发生「死锁」问题。
锁过期:客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有
释放别人的锁:客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁
导致这两个问题的原因是什么?我们一个个来看。
解决办法是:客户端在加锁时,设置一个只有自己知道的「唯一标识」进去。
SET lock $uuid EX 20 NX
// 锁是自己的,才释放
if redis.get(“lock”) == $uuid:
redis.del(“lock”)
这里释放锁使用的是 GET + DEL 两条命令,这时,又会遇到我们前面讲的原子性问题了。
用lua脚本保持原子性
// 判断锁是自己的,才释放
if redis.call(“GET”,KEYS[1]) == ARGV[1]
then
return redis.call(“DEL”,KEYS[1])
else
return 0
end
加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。
Redlock 具体如何使用呢?
整体的流程是这样的,一共分为 5 步:
1.客户端先获取「当前时间戳T1」
2.客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
3.如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
4.加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
5.加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)
几个问题?
1) 为什么要在多个实例上加锁?
本质上是为了「容错」,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。
2) 为什么大多数加锁成功,才算成功?
多个 Redis 实例一起来用,其实就组成了一个「分布式系统」。
在分布式系统中,总会出现「异常节点」,所以,在谈论分布式系统问题时,需要考虑异常节点达到多少个,也依旧不会影响整个系统的「正确性」。
这是一个分布式系统「容错」问题,这个问题的结论是:如果只存在「故障」节点,只要大多数节点正常,那么整个系统依旧是可以提供正确服务的。
3) 为什么步骤 3 加锁成功后,还要计算加锁的累计耗时?
因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在延迟、丢包、超时等情况发生,网络请求越多,异常发生的概率就越大。
所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经「超过」了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。
4) 为什么释放锁,要操作所有节点?
在某一个 Redis 节点加锁时,可能因为「网络原因」导致加锁失败。
例如,客户端在一个 Redis 实例上加锁成功,但在读取响应结果时,网络问题导致读取失败,那这把锁其实已经在 Redis 上加锁成功了。
所以,释放锁时,不管之前有没有加锁成功,需要释放「所有节点」的锁,以保证清理节点上「残留」的锁。
好了,明白了 Redlock 的流程和相关问题,看似 Redlock 确实解决了 Redis 节点异常宕机锁失效的问题,保证了锁的「安全性」。
更新数据库时通过消息队列异步删除缓存
那如果你确实不想在应用中去写消息队列,是否有更简单的方案,同时又可以保证一致性呢?
方案还是有的,这就是近几年比较流行的解决方案:订阅数据库变更日志,再操作缓存。
缓存延迟双删策略。
但问题来了,这个「延迟删除」缓存,延迟时间到底设置要多久呢?
一般为1-5s
问题1:延迟时间要大于「主从复制」的延迟时间
问题2:延迟时间要大于线程 读取数据库 + 写入缓存的时间
基数统计(Cardinality Counting) 通常是用来统计一个集合中不重复的元素个数,
B 树最大的优势就是插入和查找效率很高,如果用 B 树存储要统计的数据,可以快速判断新来的数据是否存在,并快速将元素插入 B 树。要计算基础值,只需要计算 B 树的节点个数就行了。
不过将 B 树结构维护到内存中,能够解决统计和计算的问题,但是 并没有节省内存。
bitmap 可以理解为通过一个 bit 数组来存储特定数据的一种数据结构,每一个 bit 位都能独立包含信息,bit 是数据的最小存储单位,因此能大量节省空间,也可以将整个 bit 数据一次性 load 到内存计算。如果定义一个很大的 bit 数组,基础统计中 每一个元素对应到 bit 数组中的一位。
会发现 K
和 N
的对数之间存在显著的线性相关性:N 约等于 2k。
高并发架构基石-缓存
Redis数据结构、基础知识
Redis基础知识
集群高可用、哨兵、持久化、LRU
布隆过滤器(BloomFilter)
Redis—分布式锁深入探究
Redis—跳跃表
Redis—5种基本数据结构
Redis—持久化
Reids—神奇的HyperLoglog解决统计问题
Redis分布式锁
Redis数据结构底层系列-SDS
短小精悍之 Redis 命令行工具有趣的罕见用法
Redis分布式锁(全)
Redis常见线上故障及其解决方案
缓存击穿、雪崩、穿透
布隆过滤器实战【防止缓存击穿】
分布式锁、并发竞争、双写一致性
Redis常见面试题
Redis面试题
Redis常见面试题
Redis为什么变慢了?一文讲透如何排查Redis性能问题
Redis不是一直号称单线程效率也很高吗,为什么又采用多线程了?
缓存一致性问题怎么解决?
内存耗尽后Redis会发生什么?
妈妈再也不担心我面试被Redis问得脸都绿了
知识点之外的缓存之路
缓存和数据库一致性问题
一个架构师的缓存修炼之路
再见了Antirez我永远的神
敖丙在蘑菇街的redis技术分享
课代表总结
Redis最佳实践:7个维度+43条使用规范,带你彻底玩转Redis
布隆过滤器过时了,未来属于布谷鸟过滤器?
什么鬼,面试官竟然让敖丙用Redis实现一个消息队列!!?