Redis学习笔记
性能与面试篇
Redis性能调优
前面已经介绍过两个调优方案:
- 设计上做优化:设计合理的键值,值的长度。
- 选择合适的持久化方式:4.0 选择混合持久化;如果只是做缓存,不需要设置持久化。
设置键值的过期时间(惰性删除)
- 我们应该根据实际的业务情况,对键值设置合理的过期时间,这样 Redis 会帮你自动清除过期的键值对,以节约对内存的占用,以避免键值过多的堆积,频繁的触发内存淘汰策略。
- Redis 有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
- EXPlRE命令用于将键key 的生存时间设置为ttl 秒。
- PEXPIRE 命令用于将键key 的生存时间设置为ttl 毫秒。
- EXPIREAT < timestamp> 命令用于将键key 的过期时间设置为timestamp所指定的秒数时间戳。
- PEXPIREAT < timestamp > 命令用于将键key 的过期时间设置为timestamp所指定的毫秒数时间戳。
127.0.0.1:6379> set key1 'value1'
OK
127.0.0.1:6379> expire key1 20
(integer) 1
127.0.0.1:6379> get key1
"value1"
# ...20s后
127.0.0.1:6379> get key1
(nil)
127.0.0.1:6379>
使用lazy free特性
- lazy free 特性是 Redis 4.0 新增的一个非常实用的功能,它可以理解为惰性删除或延迟删除。意思是在删除的时候提供异步延时释放键值的功能,把键值释放操作放在 BIO(Background I/O) 单独的子线程处理中,以减少删除对 Redis 主线程的阻塞,可以有效地避免删除 big key 时带来的性能和可用性问题。
- lazy free 对应了 4 种场景,默认都是关闭的:
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
slave-lazy-flush no
- lazyfree-lazy-eviction:表示当 Redis 运行内存超过最大内存时,是否开启 lazy free 机制删除;
- lazyfree-lazy-expire:表示设置了过期时间的键值,当过期之后是否开启 lazy free 机制删除
- lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作,比如rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示在这种场景中是否开启 lazy free 机制删除;
- slave-lazy-flush:针对 slave(从节点) 进行全量数据同步,slave 在加载 master 的 RDB 文件前,会运行 flushall 来清理自己的数据,它表示此时是否开启 lazy free 机制删除。
- 建议开启其中的 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置,这样就可以有效的提高主线程的执行效率。
限制Redis内存大小,设置内存淘汰策略
最大缓存设置
maxmemory 1048576 或者 maxmemory 1048576B
maxmemory 1000KB 或者 maxmemory 1000K
maxmemory 100MB 或者 maxmemory 100M
maxmemory 1GB 或者 maxmemory 1G
- 没有指定最大缓存,如果有新的数据添加,超过最大内存,则对于32位系统会使redis崩溃,所以一定要设置。最佳设置是物理内存的75%,写操作比较多则设置为60%。
- 在 64 位操作系统中 Redis 的内存大小是没有限制的,也就是配置项 maxmemory 是被注释掉的,这样就会导致在物理内存不足时,使用 swap 空间既交换空间,而当操作系统将 Redis 所用的内存分页移至 swap 空间时,将会阻塞 Redis 进程,导致 Redis 出现延迟,从而影响 Redis 的整体性能。因此我们需要限制 Redis 的内存大小为一个固定的值,当 Redis 的运行到达此值时会触发内存淘汰策略,内存淘汰策略在 Redis 4.0 之后有 8 种。
LRU原理
- LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
- 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
案例分析
以用户信息的需求为例,来演示一下LRU算法的基本思路:
- 假设我们使用哈希链表来缓存用户信息,目前缓存了4个用户,这4个用户是按照时间顺序依次从链表右端插入的。
- 此时,业务方访问用户5,由于哈希链表中没有用户5的数据,我们从数据库中读取出来,插入到缓存当中。这时候,链表中最右端是最新访问到的用户5,最左端是最近最少访问的用户1。
- 接下来,业务方访问用户2,哈希链表中存在用户2的数据,我们怎么做呢?我们把用户2从它的前驱节点和后继节点之间移除,重新插入到链表最右端。这时候,链表中最右端变成了最新访问到的用户2,最左端仍然是最近最少访问的用户1。
- 下来,业务方请求修改用户4的信息。同样道理,我们把用户4从原来的位置移动到链表最右侧,并把用户信息的值更新。这时候,链表中最右端是最新访问到的用户4,最左端仍然是最近最少访问的用户1。
- 后来业务方换口味了,访问用户6,用户6在缓存里没有,需要插入到哈希链表。假设这时候缓存容量已经达到上限,必须先删除最近最少访问的数据,那么位于哈希链表最左端的用户1就会被删除掉,然后再把用户6插入到最右端。
- 添加链接描述
LFU原理
- LFU 全称是 Least Frequency Used,最不经常使用策略。
- 在一段时间内,数据被使用频次最少的,有效被淘汰。LFU是一种用于管理计算机内存的缓存算法。主要是记录和追踪内存块的使用次数,当缓存已满并且需要更多空间时,系统将以最低内存块使用频率清除内存。
- 采用LFU算法的最简单方法是为每个加载到缓存的块分配一个计数器。每次引用该块时,计数器加一。当缓存达到容量病有一个新的内存块等待插入时,系统将搜索计算器最低的块并将其从缓存中删除。
- LRU和LFU侧重点不同,LRU主要体现在对元素的使用时间上,而LFU主要体现在对元素的使用频次上。
- LFU的缺陷是:在短期的时间内,对某些缓存的访问频次很高,这些缓存会立刻晋升为热点数据,而保证不会淘汰,这样会驻留在系统内存里面。而实际上,这部分数据只是短暂的高频率访问,之后将会长期不访问,瞬时的高频访问将会造成这部分数据的引用频率加快,而一些新加入的缓存很容易被快速删除,因为它们的引用频率很低。
Redis缓存淘汰策略
- redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。
- 例如:配置maxmemory-policy voltile-lru。支持热配置,内存淘汰策略在 Redis 4.0 之后有 8 种
名称 | 描述 |
---|---|
volatile-lru | 从已设置过期时间 的数据集中挑选最近最少使用 的数据淘汰 |
volatile-lfu | 从已设置过期时间的数据集中挑选最不经常 使用的数据淘汰(Redis 4.0之后) |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期 的数据淘汰 |
volatile-random | 从已设置过期时间的数据集中挑选任意数据 淘汰 |
allkeys-lru | 当内存不足写入新数据时淘汰最近最少使用的Key |
allkeys-random | 当内存不足写入新数据时随机选择key淘汰 |
allkeys-lfu | 当内存不足写入新数据时移除最不经常使用的Key(Redis 4.0之后) |
no-eviction | 当内存不足写入新数据时,写入操作会报错,同时不删除数据 |
- volatile为前缀的策略都是从已过期的数据集中进行淘汰。
- allkeys为前缀的策略都是面向所有key进行淘汰。
- 我们可以根据实际的业务情况进行设置,默认的淘汰策略不淘汰任何数据,在新增时会报错。
禁用长耗时的查询命令
- Redis 绝大多数读写命令的时间复杂度都在 O(1) 到 O(N) 之间,在官方文档对每个命令都有时间复杂度说明。
- 其中 O(1) 表示可以安全使用的,而 O(N) 就应该当心了,N 表示不确定,数据越大查询的速度可能会越慢。因为 Redis 只用一个线程来做数据查询,如果这些指令耗时很长,就会阻塞 Redis,造成大量延时。
- 要避免 O(N) 命令对 Redis 造成的影响,可以从以下几个方面入手改造:
- 禁止使用 keys 命令:避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;
- 通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;
- 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;
- 删除 (del) 一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。
Redis 6.0引入多线程
- 目前主流使用的Redis版本:2.8、3.2、4.0、5.0
为什么 Redis 一开始选择单线程模型(单线程的好处)?
- IO多路复用:先看下Redis的顶层设计
- FD是一个文件描述符,意思是表示当前文件处于可读、可写还是异常状态。使用 I/O 多路复用机制同时监听多个文件描述符的可读和可写状态。可以理解为具有了多线程的特点
- 一旦受到网络请求就会在内存中快速处理,由于绝大多数的操作都是纯内存的,所以处理的速度会非常地快。也就是说在单线程模式下,即使连接的网络处理很多,因为有IO多路复用,依然可以在高速的内存处理中得到忽略。
- 可维护性高:
- 多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题。单线程模式下,可以方便地进行调试和测试。
- 基于内存,单线程状态下效率依然高
- 多线程能够充分利用CPU的资源,但对于Redis来说,由于基于内存速度那是相当的高,能达到在一秒内处理10万个用户请求,如果一秒十万还不能满足,那我们就可以使用Redis分片的技术来交给不同的Redis服务器。这样的做法避免了在同一个 Redis 服务中引入大量的多线程操作。【redis并发新年测试工具】
- 而且基于内存,除非是要进行AOF备份,否则基本上不会涉及任何的 I/O 操作。这些数据的读写由于只发生在内存中,所以处理速度是非常快的;用多线程模型处理全部的外部请求可能不是一个好的方案。
- 总结成两句话,基于内存而且使用多路复用技术,单线程速度很快,又保证了多线程的特点。因此没有必要使用多线程。
为什么 Redis 在 6.0 之后加入多线程(某种情况下,单线程出现了缺点,多线程可以解决)
- 因为读写网络的read/write系统调用在Redis执行期间占用了大部分CPU时间,如果把网络读写做成多线程的方式对性能会有很大提升。
- Redis可以使用del命令删除一个元素,如果这个元素非常大,可能占据了几十兆或者是几百兆,那么在短时间内是不能完成的,这样一来就需要多线程的异步支持。
- 总结:Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上;而 Redis 引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率
使用slowlog优化耗时命令
- 我们可以使用 slowlog 功能找出最耗时的 Redis 命令进行相关的优化,以提升 Redis 的运行速度,慢查询有两个重要的配置项:
- slowlog-log-slower-than:用于设置慢查询的评定时间,也就是说超过此配置项的命令,将会被当成慢操作记录在慢查询日志中,它执行单位是微秒 (1 秒等于 1000000 微秒);
- slowlog-max-len:用来配置慢查询日志的最大记录数。
- 我们可以根据实际的业务情况进行相应的配置,其中慢日志是按照插入的顺序倒序存入慢查询日志中,我们可以使用 slowlog get n 来获取相关的慢查询日志,再找到这些慢查询对应的业务进行相关的优化。
避免大量数据同时失效
- Redis 过期键值删除使用的是贪心策略,它每秒会进行 10 次过期扫描,此配置可在 redis.conf 进行配置,默认值是
hz 10
,Redis 会随机抽取 20 个值,删除这 20 个键中过期的键,如果过期 key 的比例超过 25% ,重复执行此流程,如下图所示:
- 如果在大型系统中有大量缓存在同一时间同时过期,那么会导致 Redis 循环多次持续扫描删除过期字典,直到过期字典中过期键值被删除的比较稀疏为止,而在整个执行过程会导致 Redis 的读写出现明显的卡顿,卡顿的另一种原因是内存管理器需要频繁回收内存页,因此也会消耗一定的 CPU。
- 为了避免这种卡顿现象的产生,我们需要预防大量的缓存在同一时刻一起过期,就简单的解决方案就是在过期时间的基础上添加一个指定范围的随机数。
使用pipeline批量操作数据
- Pipeline (管道技术) 是客户端提供的一种批处理技术;可以批量执行一组指令,一次性返回全部结果;可以减少频繁的请求应答。
- 测试代码:
public class RedisPipelineTest {
/**
* 测试使用pipeline批量操作
*/
@Test
public void testPipeline() {
Jedis jedis = new Jedis("192.168.254.128", 6379);
long startTime = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
pipe.set("key" + i, "val" + i);
pipe.del("key" + i);
}
pipe.sync();
long endTime = System.currentTimeMillis();
System.out.println(String.format("执行耗时:%d ms", endTime - startTime));
}
/**
* 测试普通的操作代码
*/
@Test
public void testSimple() {
Jedis jedis = new Jedis("192.168.254.128", 6379);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
jedis.set("key" + i, "val" + i);
jedis.del("key" + i);
}
long endTime = System.currentTimeMillis();
System.out.println(String.format("执行耗时:%d ms", endTime - startTime));
}
}
客户端使用优化
- 在客户端的使用上我们除了要尽量使用 Pipeline 的技术外,还需要注意要尽量使用 Redis 连接池,而不是频繁创建销毁 Redis 连接,这样就可以减少网络传输次数和减少了非必要调用指令。
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
使用分布式架构来增加读写速度
- Redis 分布式架构有重要的手段:主从同步、哨兵模式、Redis Cluster集群
- 使用主从同步功能我们可以把写入放到主库上执行,把读功能转移到从服务上,因此就可以在单位时间内处理更多的请求,从而提升的 Redis 整体的运行速度。
- 而哨兵模式是对于主从功能的升级,但当主节点奔溃之后,无需人工干预就能自动恢复 Redis 的正常使用。
- Redis Cluster 是 Redis 3.0 正式推出的,Redis 集群是通过将数据库分散存储到多个节点上来平衡各个节点的负载压力。
- Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,计算公式:slot = CRC16(key) & 16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。这样 Redis 就可以把读写压力从一台服务器,分散给多台服务器了,因此性能会有很大的提升。
- Redis Cluster 应该是首选的实现方案,它可以把读写压力自动的分担给更多的服务器,并且拥有自动容灾的能力。
- 数据量不多,并发也不多,我们就用单机。
- 数据量不是很多,并发很多(超过10万的读) 我们一般是使用主从+sentinel 高可用。写是主库,读是从库。
- 数据量大, 我们都是采用cluster 多读写。
- cluster 把 sentinel 的功能分摊到主从节点了;监控:master,主节点是否 fail,是由主节点选择,选择新的主节点是从节点决定。
- 强调一点:Cluster的从机默认是不读不写,就是备份和容灾。
使用物理机而非虚拟机
- 在虚拟机中运行 Redis 服务器,因为和物理机共享一个物理网口,并且一台物理机可能有多个虚拟机在运行,因此在内存占用上和网络延迟方面都会有很糟糕的表现,我们可以通过./redis-cli --intrinsic-latency 100 命令查看延迟时间,如果对 Redis 的性能有较高要求的话,应尽可能在物理机上直接部署 Redis 服务器。
禁用THP特性
- Linux kernel 在 2.6.38 内核增加了 Transparent Huge Pages (THP) 特性 ,支持大内存页 2MB 分配,默认开启。
- 当开启了 THP 时,fork 的速度会变慢,fork 之后每个内存页从原来 4KB 变为 2MB,会大幅增加重写期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了 512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。例如简单的 incr 命令也会出现在慢查询中,因此 Redis 建议将此特性进行禁用,禁用方法如下:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
- 为了使机器重启后 THP 配置依然生效,可以在 /etc/rc.local 中追加
echo never > /sys/kernel/mm/transparent_hugepage/enabled
。
常见缓存问题-不单指Redis
缓存预热
- 直接写个缓存刷新页面,上线前手工操作一下。
- 数据量不大的时候,可以在项目启动的时候自动加载。
- 定时刷新缓存。
缓存雪崩
什么叫缓存雪崩?
- 当缓存服务器重启或者大量缓存集中在【某一个时间段】失效,这样在失效的时候,也会给后端系统带来很大的压力
如何解决?
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀:
// 伪代码
setRedis(key, value, time + Math.random() * 10000)
- 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效的时间设置为短期,A2设置为长期。
- 使用多级缓存:CDN、Nginx(启动 THP) ==>> 拒敌于国门之外
缓存击穿
什么叫缓存击穿?
- 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时间,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于,这里针对某一key缓存,前者则是很多key。
- 缓存在某个时间点过期的时候,恰好在这个时间点对这个key有大量的并发请求过来,这些请求发现缓存过期,一般都会从后端DB加载数据并回写缓存,这个时候大并发请求可能会瞬间把后端DB压垮
如何解决?
- 使用 redis 的 setnx 互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库
if(redis.setnx() == 1){
// 先查询缓存
// 查询数据库
// 加入缓存
}
- 设置监控:当某个热点是超级热点,一般设置为持久化。
缓存穿透
什么叫缓存穿透?
- 一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且该key并发请求量大,就会对后端系统造成很大的压力。
- 也就是说,对【不存在的 key】 进行【高并发访问】,导致数据库压力瞬间增大,这就叫做【缓存穿透】。
如何解决?
- 在服务器端,接收参数时业务接口中过滤不合法的值、null、负值和空值进行检测。
- 布隆表达式 bloom filter:类似于哈希表的一种算法,用所有可能的查询条件生成一个bitmap,在进行数据库查询之前会使用这个bitmap进行过滤,如果不在其中则直接过滤,从而减轻数据库层面的压力。采用的是一票否决,只要有一个认为你不存在,就认为你是不存在的。
- 空值缓存:一种比较简单的解决办法,在第一次查询完不存在的数据后,将该key与对应的空值也放入缓存中,只不过设定为较短的失效时间,例如几分钟,这样则可以应对短时间的大量的该key攻击,设置为较短的失效时间是因为该值可能业务无关,存在意义不大,且该次的查询也未必是攻击者发起,无过久存储的必要,故可以早点失效。
- 缓存降级的使用:我们在缓存中没有数据的时候,会给一个随机值。 水贴,随贴。
缓存降级
- 当访问量出现剧增、服务出现问题(相应时间慢或者不响应) 或非核心业务影响到核心流程的性能,还需要保证服务的可用性,即便有损服务。
- 双11的时候,一般会对 退款 降级。
- 方式:系统根据一些关键数据进行降级;配置开关实现人工降级。
- 有些服务时无法降级(加入购物车,结算)
- 参考日志级别:
- 一般:ex有些服务偶尔网络抖动或者服务正在上线超时,可以自定降级;
- 警告:有些服务在一端时间内有波动(95%-100%),可以自定降级或人工降级,还有发送告警;
- 错误:可利用率低于90%,redis连接池被打爆了,数据库连接池被打爆,或者访问量突然猛增到系统能承受的最大阈值,这时候根据情况自动降级或人工降级;
- 严重错误:比如因为特殊原因数据错误了,需要紧急人工降级。
redis服务出问题了, 不去查数据库,而是直接返回一个默认值(自定义一些随机值)
缓存更新
- 自定义的缓存淘汰策略:定期去清理过期的缓存;当有用户请求过来时,先判断这个请求用到的缓存是否过期,过期的话就去底层系统得到新数据进行缓存更新。
缓存数据库双写一致性
- 双写 就一定会出现数据一致性问题
- 一般来说,在读取缓存方面,我们都是先读取缓存,在读取数据库的
- 但是,在更新缓存方面,我们是需要先更新缓存,再更新数据库?还是先更新数据库,再更新缓存?还是说有其他方案?
先更新Cache再更新DB(不建议)
- 操作步骤(线程A和线程B都对同一数据库进行更新操作)
1. 线程A更新了缓存
3. 线程B更新了缓存 ====> 缓存中现在是B的数据
4. 线程B更新了数据库
5. 线程A更新了数据库 ====> 数据库中是A的数据
先更新DB再更新Cache(不建议)
- 操作步骤(线程A和线程B都对同一数据库进行更新操作)
1. 线程A更新了数据库
2. 线程B更新了数据库 ====> 数据库现在是B的数据
3. 线程B更新了缓存
4. 线程A更新了缓存 ====> 缓存中是A的数据
- 问题1:脏读
- 问题2:浪费性能
先更新DB再删除Cache
- 操作步骤(线程A更新、线程B读)
1. 请求A进行写操作、删除缓存,此时A的写操作还没有执行完(缓存删除成功,但是数据库操作还没成功)
2. 请求B查询发现缓存不存在
3. 请求B去数据库查询得到旧值
4. 请求B将旧值写入缓存 ====> 缓存中数据是B的(旧值)
5. 请求A将新值写入数据库 ====> 数据库中数据是A的新值
- 解决方案1:延时双删,伪代码如下
public void write(String key, Object data){
redis.delKey(key);
db.updateDataa(data);
Thread.sleep(1000);
redis.delKey(key);
}
- 解决方案2:将请求存入到消息队列
先删除Cache再更新DB
- 操作步骤
1. 用户A删除缓存失败 ====> 缓存中数据是旧值
2. 用户A成功更新了数据 ====> 数据库中数据是A的新值
或者
1. 用户A删除了缓存;
2. 用户B读取缓存,缓存不存在;
3. 用户B从数据库拿到旧数据;
4. 用户B更新了缓存; ====> 缓存中数据是B的(旧值)
5. 用户A更新了数据。 ====> 数据库中数据是A的新值
- 问题:脏数据
- 解决方案1:设置缓存有效时间(最简单)
- 解决方案2:使用消息队列
多个系统同时操作(并发)Redis带来的数据问题
- 系统A、B、C三个系统,分别去操作Redis的同一个Key,本来顺序是1,2,3是正常的,但是因为系统A网络突然抖动了一下,B,C在他前面操作了Redis,这样数据不就错了么。
- 就好比下单,支付,退款三个顺序你变了,你先退款,再下单,再支付,那流程就会失败,那数据不就乱了?你订单还没生成你却支付,退款了?明显走不通了,这在线上是很恐怖的事情。
- 这种情况怎么解决呢? ==>> 可以找个管家帮我们管理好数据的嘛!
- 某个时刻,多个系统实例都去更新某个 key。可以基于 Zookeeper 实现分布式锁。每个系统通过Zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 Key,别人都不允许读和写。
- 要写入缓存的数据,都是从 MySQL 里查出来的,都得写入 MySQL 中,写入 MySQL 中的时候必须保存一个时间戳,从 MySQL 查出来的时候,时间戳也查出来。
- 每次要写之前,先判断一下当前这个 Value 的时间戳是否比缓存里的 Value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。
Redis常见面试问题
Memcache特点
- MC 处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,性能非常优秀;
- MC 功能简单,使用内存存储数据;
- MC 对缓存的数据可以设置失效期,过期后的数据会被清除;
- 失效的策略采用延迟失效,就是当再次使用数据时检查是否失效;
- 当容量存满时,会对缓存中的数据进行剔除,剔除时,除了会对过期 key 进行清理,还会按 LRU策略对数据进行剔除。
- MC 有一些限制,这些限制在现在的互联网场景下很致命,成为大家选择Redis、MongoDB的重要原因:
- key 不能超过 250 个字节;
- value 不能超过 1M 字节;
- key 的最大失效时间是 30 天;
- 只支持 K-V 结构,不提供持久化和主从同步功能;
- MC没有原生的集群,可以依靠客户端实现往集群中做分片写入数据。
Redis特点
- 与 MC 不同的是,Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
- Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。
- 相比 MC,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
- Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。
MC和redis的性能对比
- 存储小数据时 Redis性能是比MC性能高;
- 100K以上;MC的性能是高于Redis。
- MC本身没有集群功能,可以使用客户端做分片。
历史面试问题(回顾)
Redis 有哪些数据类型?
- string、hash、list、set、zset、bitmap、hyperloglog、geospatial、。。。
能说一下他们的特性,还有分别的使用场景么?
单机会有瓶颈,怎么解决这个瓶颈?
- 集群:主从、哨兵、Cluster
他们之间是如何进行数据交互的?
- 主从复制-RDB
Redis是如何进行持久化的?
- 持久化方式:RDB、AOF;4.0后增加了混合持久化
如何选择持久方式的?
- 4.0之前 只是做缓存 RDB 其他是AOF和RDB都打开,不建议只使用AOF 4.0之后需要使用 就开启混合持久化 。混合持久化是对AOF的一个补充,主要是重写不一样
Redis还有其他保证集群高可用的方式吗?
- 主从+哨兵
数据传输的时候网络断了或者服务器断了,怎么办?
- 增量同步、全量同步
能说一下Redis的内存淘汰机制么?
- LRU、LFU
如果的如果,定期没删,我也没查询,那可咋整?
- 设置最大内存,还要设置缓存淘汰策略
哨兵机制的原理是什么?
- 监控-3个任务;判断下线-SDOWN和ODWON;选举 leader;自动故障迁移
哨兵组件的主要功能是什么?
- 监控、通知(告警)、自动故障迁移
Redis的事务原理是什么?
- 内存队列,批量处理,具有隔离性
Redis事务为什么是“弱”事务?
- 不是原子操作、不支持回滚
Redis为什么没有回滚操作?
- 大多数错误都是代码和数据的问题 ,可以预判断;性能考虑
在Redis中整合lua有几种方式,你认为哪种更好,为什么?
- 写lua文件 redis.call()
lua如何解决Redis并发问题?
- 原子性(隔离性)
介绍Redis下的分布式锁实现原理、优势劣势和使用场景?
Redis-Cluster和Redis主从+哨兵的区别,你认为哪个更好,为什么?
- 具体看业务
- 如果我们读多写少 并发量大 数据只有2G 100W读 主从+哨兵
- 如果并发写多 数据量大 40G数据,10W并发 Rediscluster
什么情况下会造成缓存穿透,如何解决?
- 缓存没数据,所有操作访问数据库。
- 服务器过滤 缓存降级 布隆过滤器
什么情况下会造成缓存雪崩,如何解决?
- 大量key同时失效,又有大量并发访问。
- 解决:过期时间+随机值 多级缓存,CDN和Nginx
什么是缓存击穿,如何解决 热点key失效?
- 持久化 二级缓存
什么情况下会造成数据库与Redis缓存数据不一致,如何解决?
- 只要双写就会不一致。
- 一种最终一致性: 延迟删除 mysql binlog MQ 异步更新KV
- 还有一种强一致性:KV-DB 必须保持一致 读请求和写请求串行化,传到内存队列。 比正常情况服务器多使用5.6-5.7倍
那你了解的最经典的KV,DB读写模式什么样?
- CAP模式
- 读的时候 先读缓存,缓存没有的话 就读数据库,然后把取出的数据放入到缓存 ,同时返回响应
- 更新的时候,先更新数据库,然后再删除缓存
为什么是删除缓存,而不是更新缓存?
- 用到缓存的时候再去算缓存
- Lazy 思想
Redis的线程模型你了解吗?
- 文件处理器 由于文件处理器是单线程的 所以我们说redis是单线程。
- Redis的核心组件:
- Socket
- 多路复用程序
- 文件命令分配(分派)器
- 文件处理器(命令接受器,命令处理器,命令响应处理器)