参考和摘自:
中华石杉 《Java工程师面试突击第1季》
Redis线程模型(简单易懂)
Redis线程模型(详细)
分布式缓存技术redis系列
高性能、高并发
不加缓存每个请求都会去访问数据库,数据库的性能成了整个系统的瓶颈。如果在业务层加入缓存,相同的数据请求第一次访问数据库后放入缓存中,再次请求就从缓存中取得数据,不再深入到数据库去拿数据。缓存取得数据会比数据库快得多,这是高性能。如果并发量很大,数据库承载不住大量的请求,缓存存取速度很快,可以减少数据库访问,能适当支撑住大量请求,这是高并发。
以下摘自 中华石杉 《Java工程师面试突击第1季》
Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。
内存使用效率对比:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。
性能对比:由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。
集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是redis目前是原生支持cluster模式的,redis官方就是支持redis cluster集群模式的,比memcached来说要更好
纯内存、NIO、单线程
纯内存操作
核心是基于非阻塞的 IO 多路复用机制
单线程避免了多线程上下文切换带来的消耗
文件事件处理器(file event handler):
图引自Redis线程模型 稍加修改
总结:客户端通过多个Socket发起的连接、写入、读取等事件均被IO多路复用程序监听到,IO多路复用程序把事件放入队列,文件事件分派器将事件取出后和相应的“处理器”(连接应答处理器、命令请求处理器、命令回复处理器、复制处理器(主从复制)等等…)关联并处理,一次只处理一个事件。
I/O 多路复用程序允许服务器同时监听Socket的 AE_READABLE 事件和 AE_WRITABLE 事件, 如果一个Socket同时产生了这两种事件, 那么文件事件分派器会优先处理 AE_READABLE 事件, 等到 AE_READABLE 事件处理完之后, 再处理 AE_WRITABLE 事件。这也就是说, 如果一个Socket即可读又可写的话, 那么服务器将先读后写。
说明:Redis针对于每个实际操作都是在内存中的,超级快,此处不是Redis瓶颈。单线程每次从队列中取得一个任务处理避免了使用多线程上下文切换而带来的不必要消耗,同时也规避了多线程并发带来的竞争问题(多线程读写同一条数据)。Redis瓶颈是网络IO读写,此处采用NIO非阻塞多路复用模型,最大化IO效率。
定期删除 + 惰性删除
定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键。
如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25ms。
如果之前回收键逻辑超时,则在Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时时间为1ms且2s内只能运行1次。
快慢两种模式内部删除逻辑相同,只是执行的超时时间不同。
maxmemory-policy参数控制的内存淘汰机制
noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用。最常用
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。
参考自: 内存溢出控制策略
待补充
待补充
Cache Aside Pattern 原则有以下几个步骤:
在低并发时,上述原则可用。如果高并发,在⑤处产生读并发,写请求还没来得及更新数据库,读请求读取数据库并把数据放到缓存后,写请求修改了数据库。这时由于并发问题导致了数据库和缓存不一致问题。
解决方案:
第一种:用锁控制,拿到锁的执行,没拿到的等待。
Redisson分布式读写锁可以解决,把②③用读锁包裹起来,把④⑤用写锁包裹起来。但是存在两个问题:
第二种:把任务放入队列串行执行。
建立多个内存队列,用hash取模的方式将同唯一标识(订单号、商品ID等)的请求路由到同一个内存队列中等待执行。如果订单服务是集群,需要考虑把同订单号请求路由到同一个服务器再路由到同一队列。在队列中排队的服务需要有过期时间,防止等待过长时间。在入队前检查队列中前一个同订单号操作是否是读操作,若果是直接返回并自旋等待缓存更新。(思路和加锁相同,这种方式可以让写请求有公平的机会执行)比较可行
三个服务节点查询DB后持有三个版本的数据并发更新Redis同一个key,最后的结果由于竞争保证不了最终一致性。
解决方案:DB设置数据版本字段(时间戳 等),写入Redis前先获取分布式锁保证接下来操作的原子性:比较Redis中的时间戳,并写入比较新版本的数据。
最终所得结果总是最新版本的数据。
场景:Redis集群挂掉了或者由于网络原因部分不可用,导致大量请求落到数据库上,数据库被压垮,导致整个系统不可用。
解决办法:
场景:缓存中大量key同一时间失效,大量请求同时落到数据库上。
解决办法:
场景:大量请求用不存在的ID访问,比如查找ID为-1000的商品。这时候请求先查找缓存,缓存未命中,之后查数据库,数据库也未命中,之后返回空结果。 如果再次用这个ID访问,请求最终还是会落到数据库上,造成“缓存穿透”。
解决办法:
参考:BloomFilter(大数据去重)+Redis(持久化)策略
场景:对同一个缓存key大量并发,恰巧这个key失效了,请求落库,压力剧增。
解决办法:
场景:刚刚启动项目时没有缓存,大量请求直接落库。
解决办法: