高级JAVA开发 Redis部分

Redis

参考和摘自:
中华石杉 《Java工程师面试突击第1季》
Redis线程模型(简单易懂)
Redis线程模型(详细)
分布式缓存技术redis系列

缓存的作用、为什么要用缓存

高性能、高并发

不加缓存每个请求都会去访问数据库,数据库的性能成了整个系统的瓶颈。如果在业务层加入缓存,相同的数据请求第一次访问数据库后放入缓存中,再次请求就从缓存中取得数据,不再深入到数据库去拿数据。缓存取得数据会比数据库快得多,这是高性能。如果并发量很大,数据库承载不住大量的请求,缓存存取速度很快,可以减少数据库访问,能适当支撑住大量请求,这是高并发。

Redis和Memcached区别

以下摘自 中华石杉 《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来说要更好

Redis五种数据类型和使用场景

  1. String:简单的String类型,用key取value。
  2. Hash:可以想象成是一个Map,存储对象,比如一个学生等等。可以取出或修改整个对象,也可以单独取出或者修改对象中的一个字段。
  3. list:有序列表。可以做通过lrange等指令做分页。可以在两端存取元素,基于这个可以实现消息队列。
  4. set:无序集合,自动去重。可以求两个set的交集、并集、差集等操作。
  5. sorted set:排序的set,放入元素时候可以指定元素的score,它会按照score顺序自动排序。

Redis线程模型(为什么Redis是单线程的但是还可以支撑高并发)

纯内存、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效率。

Redis过期策略、内存回收策略(内存淘汰机制)

定期删除 + 惰性删除

  1. 惰性删除:读取到带有过期时间的key时,如果数据已经过期那么将删除掉数据并返回空。
  2. 定时任务删除:Redis内部默认维护了一个1秒10次的定时任务(可以通过配置修改执行频率)定时任务中删除过期键逻辑采用了自适应算法,根据键的过期比例,使用快慢两种速率模式回收键。
    比如:

定时任务在每个数据库空间随机检查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 优先移除。

参考自: 内存溢出控制策略

Redis的高可用架构(主从replication、哨兵、cluster集群)

待补充

Redis的持久化机制(RDB、AOF)

待补充

使用缓存带来的问题以及处理办法

缓存与数据库双写一致性问题

Cache Aside Pattern 原则有以下几个步骤:

  1. 读的时候,①先读缓存,缓存命中则直接返回,缓存没命中那么就②读数据库,取出数据后③放入缓存,最后返回响应。
  2. 更新的时候,先④删除缓存,然后再⑤更新数据库。

在低并发时,上述原则可用。如果高并发,在⑤处产生读并发,写请求还没来得及更新数据库,读请求读取数据库并把数据放到缓存后,写请求修改了数据库。这时由于并发问题导致了数据库和缓存不一致问题。

解决方案:

  1. 低要求方案(自己想的):更新的时候,先④删除缓存,再⑤更新数据库,⑥再删除一次缓存。对缓存一致性要求不是很高的情况可以采用。在⑥前并发的读请求拿到的是旧数据,而等⑥删除缓存后, 读请求拿到的数据是新的。
  2. 高要求方案:
    思路:由于没保证操作的原子性而引发了问题。竞争问题解决办法就是一次只能有一个任务执行,一般两种思路:
    • 第一种:用锁控制,拿到锁的执行,没拿到的等待。
      Redisson分布式读写锁可以解决,把②③用读锁包裹起来,把④⑤用写锁包裹起来。但是存在两个问题:

      1. Redisson实现的读写锁不是公平的(用一个分布式公平读写锁,自认为这个方案是比较完美的~),这里写线程数量会大大少于读线程数量,写线程数量得到锁的概率很小很小,修改操作成功率非常低(要么一直死等下去,要么超时返回错误,再来请求还是超时),这么做可以保证数据的一致性。
      2. 写锁释放后,此时已经积压了大量的读请求,所有读请求一同拿到锁后一起查询数据库后一起填充缓存,解决这个问题可以再引入一个分布式互斥锁,用tryLock方式尝试获取锁,获取到了执行②③,没获取到就自旋访问缓存,直到缓存被拿到互斥锁的线程填充。需要考虑一个完整访问数据库的操作需要多长时间,在这段时间内有多少请求压在自旋中,会不会大量占用服务器cpu资源。个人总结的办法,由于不是公平读写锁,不是很可行
    • 第二种:把任务放入队列串行执行。
      建立多个内存队列,用hash取模的方式将同唯一标识(订单号、商品ID等)的请求路由到同一个内存队列中等待执行。如果订单服务是集群,需要考虑把同订单号请求路由到同一个服务器再路由到同一队列。在队列中排队的服务需要有过期时间,防止等待过长时间。在入队前检查队列中前一个同订单号操作是否是读操作,若果是直接返回并自旋等待缓存更新。(思路和加锁相同,这种方式可以让写请求有公平的机会执行)比较可行

Redis并发竞争问题

三个服务节点查询DB后持有三个版本的数据并发更新Redis同一个key,最后的结果由于竞争保证不了最终一致性
解决方案:DB设置数据版本字段(时间戳 等),写入Redis前先获取分布式锁保证接下来操作的原子性:比较Redis中的时间戳,并写入比较新版本的数据。
最终所得结果总是最新版本的数据。

缓存雪崩问题

场景:Redis集群挂掉了或者由于网络原因部分不可用,导致大量请求落到数据库上,数据库被压垮,导致整个系统不可用。

解决办法:

  1. 保证Redis采用高可用架构,主从+哨兵,redis cluster,避免全盘崩溃
  2. 系统采用多级缓存,比如用ehcache做应用内缓存。
  3. 用 hystrix 限流、熔断、降级,避免Mysql被压死。
  4. Redis配置持久化,如果出故障可以快速恢复。

热点数据集中失效(另一种雪崩)

场景:缓存中大量key同一时间失效,大量请求同时落到数据库上。

解决办法:

  1. 设置过期时间时,在过期时间后加上一个随机值,避免时间相同。
  2. 同一条数据加互斥锁,第一个请求可以通过,之后的请求稍等一下并轮训等待、检查缓存是否存在。这样吞吐量会下降,需要考虑场景。

缓存穿透问题

场景:大量请求用不存在的ID访问,比如查找ID为-1000的商品。这时候请求先查找缓存,缓存未命中,之后查数据库,数据库也未命中,之后返回空结果。 如果再次用这个ID访问,请求最终还是会落到数据库上,造成“缓存穿透”。

解决办法:

  1. 不存在的ID第一次请求后写入Redis特殊标识返回结果为UNKNOWN,第二次请求拿到特殊标识后返回。这样请求不会落到数据库上了。
  2. 非法key用程序过滤掉,尽量减少。
  3. 利用BloomFilter挡掉不存在的请求。将存在的key计算后存入BloomFilter,取值时先检查key在BloomFilter是否存在,一定不存在的直接挡掉。

参考:BloomFilter(大数据去重)+Redis(持久化)策略

缓存击穿问题

场景:对同一个缓存key大量并发,恰巧这个key失效了,请求落库,压力剧增。

解决办法:

  1. 加互斥锁。第一个请求更新缓存,后续请求等待过程中检查缓存。比如Redis分布式锁。
  2. 不给key设置有效期,永久有效。用定时任务去更新缓存。
  3. 逻辑过期,把过期时间设置在数据中。设置的过期时间略小于实际过期时间。取到值时实际数据并未过期。取值时校验,过期就另起异步线程更新缓存。当前数据直接返回给前端。

缓存预热

场景:刚刚启动项目时没有缓存,大量请求直接落库。

解决办法:

  1. 启动时候先加载缓存,加载成功后再提供服务。
  2. 写个定时任务,定时刷新缓存。
  3. 写定时任务,把页面刷一遍。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章