1. Redis是什么?
Redis是由C语言编写、基于内存、支持多种数据结构、高性能的Key-Value数据库。
它支持string,list,set,sorted set,hash等数据结构;支持数据持久化,防止重启后数据清空;支持主从备份;事务等
2. 为什么使用Redis?
项目中使用redis,主要考虑两个方面:并发和性能。redis也可以充当中间件,但是也有其他专门的中间件代替,而且它的特长并不体现在这里。
2.1 并发
在大并发的情况下,所有的请求直接访问数据库,数据库的读写方式是磁盘读/写,速度相对比较慢,此时要求数据库短时间完成大量访问,很有可能造成数据库宕机,所以引入基于内存进行读写操作的redis。
逻辑:高并发情况下,请求到达服务器,只把业务数据在redis上进行读写,而不对数据库做操作,完成逻辑后将redis缓存数据一次性写入数据库,这样来提高性能。(在内存操作数据完成后,最后操作磁盘)
注意:缓存中数据仍要落库持久化,所以一个请求在redis上完成读写操作后,判断业务逻辑是否完成,比如秒杀商品数目为0,红包金额为0,将redis缓存数据一次性写入数据库,完成持久化。否则不操作数据库
2.2 性能
一般访问数据库,读操作要多于写操作,如果每次都访问数据库,会相对很慢,所以把数据放缓存里读写会快很多,并缓解数据库压力。
逻辑:当第一次访问数据库,读取redis数据肯定为空,此时就读取数据库,并放到redis(注意非空判断)。之后每次读取数据,都直接读取redis,这样来提高系统性能。(此时考虑写操作DB和redis数据一致的问题,见下文)
注意:如果数据并不常用,写操作频率大于读操作,考虑成本如占存储很大的数据,这些就不需要放在redis缓存中。redis只存储业务常用的数据和主要的数据,比如用户登录信息。
3. 其他常见问题
1. 单线程的redis为什么这么快
参考:https://blog.csdn.net/xlgen157387/article/details/79470556
参考:https://redis.io/topics/benchmarks
- 纯内存操作
- 单线程操作,避免了频繁的上下文切换
- 采用了异步非阻塞 IO
- 高效的数据结构
非阻塞 I/O 多路复用机制:按照我的理解,只有一个线程,根据每个I/O流的状态,来管理多个I/O流。如快递员送货,但只有一个快递员,送货地点有很多,需要标记每个送货点的地址,然后送一个快递再回到取货点拿下一个快递送到下一个地点。
2. redis的数据类型,以及每种数据类型的使用场景
参考:https://segmentfault.com/a/1190000012212663
- String:缓存、计数器、分布式锁等。
- List:链表、队列、微博关注人时间轴列表等。
- Hash:用户信息、Hash 表等。
- Set:去重、赞、踩、共同好友等。
- Zset:访问量排行榜、点击量排行榜等。
3. redis的过期策略以及内存淘汰机制
参考:https://blog.csdn.net/qq_28018283/article/details/80764518
如redis只能存5G数据,可是你写了10G,那会删5G的数据。redis是如何删除的?如果数据设置了过期时间,但是时间到了,内存占用率还是比较高,是什么原因?
如果采用定时删除,定时器来监视key,过期定时删除,这样十分消耗CPU,所以redis未采用此方式(X)
redis采用的是定期删除+惰性删除策略(✔)。
- 定期删除:redis默认每个100ms检查,是否有过期的key,有过期key则删除。但redis并不是对所有key进行检查,而是随机进行检查,所以很多key在过期后尚未被删除,所以需要惰性删除
- 惰性删除:获取某个key的时候,redis先检查此key是否过期了,如果过期了此时再删除。
此时如果并没有去获取此key(惰性删除未生效),redis的内存就会越来越高,此时采用内存淘汰机制。在redis.conf配置文件中,有一行配置
# maxmemory-policy volatile-lru
即配置内存淘汰策略,推荐:allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key
4. 数据库和redis的数据如何保持同步
参考:https://www.cnblogs.com/rjzheng/p/9041659.html
首先要配置缓存过期时间,在过期后再进行操作就会根据数据库操作,不存在同步问题。此时,讨论在未过期这段时间的同步问题。
一般有三种策略,根据具体业务场景选择
- 先更新数据库,再更新缓存(不考虑)
- 先删除缓存,再更新数据库(不推荐)
- 先更新数据库,再删除缓存(推荐)
-
第一种 先更新数据库,再更新缓存
假设请求A和请求B进行更新操作,可能会出现
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
此时应该A更新缓存应该比B更新缓存早才对,但由于网络等原因,B却比A更早更新了缓存。这就会产生脏数据。
而且如果写操作比较多,读操作较少,这种方案会导致数据库和缓存会被频繁的更改,但很少读取,浪费性能
-
第二种 先删除缓存,再更新数据库
假设请求A进行更新,请求B进行读取,
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
即在线程A写操作的程序中,删除缓存和更新数据库的过程中,线程B又读取数据库原数据重新更新了缓存,出现了脏数据。此时,可以使用延时双删策略。
redis.delKey(key); //删除缓存
db.updateData(data); //更新数据库
Thread.sleep(1000); //延时等待时间根据读操作的耗时时间加几百ms
redis.delKey(key); //再次删除缓存
但延时时间影响到吞吐量降低(第二次删除可以另开启一线程异步删除来解决),而且第二次删除失败怎么办?
-
第三种 先更新数据库,再删缓存
先把数据落库更新,成功后再删除缓存。但仍存在很小概率的脏数据事件:
假设请求A查询,请求B更新
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
对于缓存过期的问题,可以进行缓存预热,即在过期时,异步线程读取数据库,更新缓存。
此时仍可以异步延时删除策略,但第二种的二次删除失败问题仍可能存在。
解决:
(1)如果对一致性要求不太高,可以在程序中另起一线程每个一段时间重试删除
(2)可以使用订阅binlog程序(如mysql的canal),如果删除失败,从订阅程序中提取所需的key,可以将信息发送至消息队列,判断消息队列有该数据,就进行删除。
5. redis持久化问题
参考:https://segmentfault.com/a/1190000002906345
持久化是为了解决进程退出(如重启,宕机,断电等原因),防止内存中数据清空,将其保存在磁盘上。
redis有两种持久化方式:快照(RDB,默认
)和追加式文件(AOF
)。(还有不常用的两种:虚拟内存方式和diskstore方式)
- RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)
- AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。
- Redis 可以同时使用 AOF 持久化和 RDB 持久化。 此时,当 Redis 重启时优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集频率更高,更完整。但RDB性能较好
6. 缓存穿透,缓存击穿和缓存雪崩问题
参考:https://blog.csdn.net/zeb_perfect/article/details/54135506
- 穿透:长时间请求缓存中不存在的数据,请求就会到达数据库,如果并发量很大,可能会导致数据库宕机或其他连接异常问题
- 击穿:某个key设置了过期时间,此时在过期的这个时间点对Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这时大并发的请求可能会瞬间把后端DB压垮
- 雪崩:缓存突然集体失效(如对key设置了相同的过期时间),此时再次大量请求,也可能会数据库连接异常
7. Redis锁
参考:http://www.360doc.com/content/18/0528/08/36490684_757590223.shtml
参考文章:
深度学习redis系列:https://www.cnblogs.com/kismetv/p/8654978.html#t6