Redis和Memcache的主要区别
- 1.数据结构方面,Redis的key-value存储,value支持string,list,set,sorted set,hash等多种数据结构。Memcache只支持简单的数据类型value为string类型。
- 2.数据安全方面,Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
- 3.Redis是非阻塞单线程IO复用模型,Memcache是非阻塞多线程IO复用模型。
Redis数据结构主要的使用场景
- list,存储一个先入先出的value列表,一个Map<String,List结构,list先进先出,可以做类似队列这种结构,另外还可以提供无序的简单分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。
- map,存储一个value kv列表,双层map结构Map<String,Map<String,List结构,存储的结构较为丰富。
- set,一个无序的去重value列表,不允许重复 ,Map<String,Set结构,可以在分布式系统下做交集,并集,差集等操作,比如可以看两个微博博主的共同粉丝,可以放在redis里面做。
- sorted set,一个有序的去重value了列表,Map<String,SortedSet结构,根据object某个字段排序,也可以做有序的分页操作。
Redis的过期策略和内存淘汰机制
Redis过期策略:定期删除+惰性删除
实际应用场景中,如果redis缓存中存在大量的数据,超过了过期时间之后,如果遍历每个key去判断过期时间删除,会造成cpu负载过大。redis采用定期删除+惰性删除过期策略,定期删除只是随机的扫描一些过期的k-v进行删除,惰性删除是指在redis中进行get某个key的时候,判断是否过期,如果过期了,直接删除,并且不返回给客户端返回任何内容。
- 获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。
Redis内存淘汰机制:
基于redis的定期删除和惰性删除的过期策略,删除的速度可能并没有redis缓存增加速度快,这个时候redis需要进行内存淘汰,基本基于LRU(最近最少使用原则),最近最少使用的数据被删除:
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
可以基于LinkedHashMap实现一个最近最少使用原则:
1 | class LRUCache<K, V> extends LinkedHashMap<K, V> { |
缓存雪崩
场景:对于某个系统,高峰时间有5000个请求,原本都可以打到缓存上,但是缓存意外宕机,这时所有的缓存查询打到数据库上,导致数据库雪崩。通常我们称之为缓存雪崩。缓存雪崩如何应对:
- 事前:redis高可用,主从+哨兵,防止全盘崩溃。
- 事中:mysql限流+降级。
- 事后:redis数据持久化,宕机之后立即重启恢复数据。
缓存穿透
黑客恶意攻击,查询缓存key永远不命中,将大量请求打到数据库,通过每次缓存查询不到时,对当前查询的key增加一个空的值。
缓存和数据库双写不一致
缓存和数据库需要保持一致,否则引起脏读。数据库更新和缓存更新的先后顺序。
为什么是删除缓存而不是更新缓存
更新缓存的动作可能非常频繁,但是缓存还是要以命中率为指标。可能更新动作频繁的数据实际上是一个读的冷数据,这样导致更新操作频繁浪费资源,可以用懒写的这种方式。也就是当你要更新缓存的时候,不更新,直接删除。读缓存命中失败之后从数据库磁盘读,顺便写进缓存。执行顺序:
- 先删除缓存后更新数据库,默认第一步失败抛出异常,第一步必成功,第二步更新数据库即使失败,不影响一致性。
- 先更新数据库后删除缓存,第二步失败,会影响一致性。
极端情况下,即使是先删除缓存后更新数据库,这两个操作始终不是一个事务。可能新的值还没有写到数据库,但是旧值已经加载到缓存,导致了缓存不一致。
高并发下通过内存队列保持一致性
高并发下需要保证缓存不命中的时候读操作等待更新操作完成,这里保证缓存和数据库双写的一致性,可以通过维护一个更新操作的内存队列。这个内存队列保证分布式集群部署条件,可以考虑用redis:
- 1.更新操作到达redis,先删除对应缓存。
- 2.根据更新数据的唯一标示,删除缓存通识增加一个更新队列到redis中。key为数据唯一标示。
- 3.读操作到达redis,查询缓存未命中,查询内存队列,有更新操作需要等待,超时时间内等待。
- 4.更新队列更新数据库操作完成,清空内存队列。
- 5.读操作循环检测内存队列,直到内存队列为空,开启查询,读操作设置超时时间,超时从数据库读取旧值。
实际上在高并发的场景下,我们保证了一致性就必定是不能保证高可用的。如果更新操作频繁,将会导致读操作超时阻塞等待时间比较久。实际场景也需要做大量的测试。
分布式寻址算法
先介绍下分布式寻址一般采用的几种方式:
分布式寻址算法主要针对将数据分成多片,适合海量数据+高并发+高可用这种场景。无法对key进行范围查找,单片对范围查找支持比较友好。
针对这种场景可以采用hash算法,一致性hash算法或者hash slot算法。目前redis cluster采用的是hash slot算法。这里假设场景是,多个master,每个master上存储了部分元数据,同时每个master都有自己的slave集群,结合sentinel哨兵机制去做高可用。
hash算法(导致缓存穿透)
- 1.根据数据的key值进行hash,得到唯一的hash值,再通过取模,得到具体要去哪个master寻数据。
- 2.如果出现宕机,取模的数量会减少一,导致大量的数据都是寻址到不对应的master上,缓存命中失败,缓存穿透。
一致性hash算法(虚拟节点负载均衡)
一致性hash算法,针对上面的hash算法进行了处理:
- 1.先定制一个hash环,hash值可能从0~2^32-1,所有的master节点都会被映射环的任意位置。
- 2.假设我们当前有八个master节点,每个节点开始在环上寻找自己的位置,根据主机ip+端口得到的hash值占据环上某个位置。
- 3.当查询或者更新的key进来时,同样根据key去做hash得到hash值落在环上某个点,顺时针找到第一个master节点。
- 4.节点数量很少的情况下可能导致节点过于分散,出现单个master节点负载过高,采用虚拟节点进行负载均衡,在step2对节点进行hash的时候,单个master进行多次hash产生多个虚拟节点,尽量均匀分散。
redis cluster的hash slot算法
redis cluster提出了哈希槽的概念,没有采用一致性hash算法。一致性hash算法具备很好的数据容错性和扩展性,当某一个节点宕机时,只有宕机的节点历史数据会受到影响,新数据会容错到其他的节点上,而且如果需要增加和删除节点,也是非常方便的。
redis cluster采用的了2^14(16834)个hash slot,这些slot会根据节点自动分片,当删除节点和增加节点的时候会将节点的hash slot进行重新分配,动态增加和减少节点不影响整个集群的可用性。
redis高可用和主备切换,可以类比哨兵机制replication+sentinel,主要的步骤:
- 1.判断master节点宕机,直接ping不过,主观宕机,多数节点ping master不过,客观宕机,认定master宕机。
- 2.slave节点过滤,通过slave的连接master时间失联的长短过滤掉失联过长的slave节点。
- 3.slave节点选举,选举出数据最多的某个slave作为新的master节点,进行故障转移。
整个过程跟哨兵机制是高度一致的。
redis分布式锁和zookeeper分布式锁
redis分布式锁加锁在当前redis cluster的部署机制下,需要对某个master的节点遍历加锁才算加锁成功,并且其他的线程需要等待,底层是线上较zookeeper更为复杂。通常我们采用zookeeper做分布式锁实现较为简单。
- redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。获取到锁的客户端挂了其他客户端需要等待锁释放。
- zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。获取到锁的客户端挂了不需要等待,直接释放。