redis缓存学习笔记、双写一致性

为什么要用缓存?

  • 高性能、高并发

redis 和 memcached 有啥区别?

  • redis支持复杂的数据结构
  •  redis原生支持集群模式, memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。
  • redis 只使用单核,而 memcached 可以使用多核,每一个核上 redis 在存储小数据时比 memcached 性能更高。在 100k 以上的数据中,memcached 性能要高于 redis

redis 的线程模型:

  • 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以redis才叫做单线程的模型。
  • 文件事件处理器的结构包含 4 个部分:
  1. 多个 socket
  2. IO 多路复用程序
  3. 文件事件分派器
  4. 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个socket可能会并发操作产生不同的操作,redis端IO多路复用程序(一个线程)监听多个server socket,将socket产生的事件放入队列中排队,事件分派器每次从队列中去一个事件,交给对应的事件处理器处理。

redis缓存学习笔记、双写一致性

个人理解:redis的线程模型和netty的线程模型相似,监听发起者和被监听者正好相反:

  • redis的一个线程监听多个应用的socket。(用户线程通过select监听netty线程)
  • netty则是用户线程通过select监听netty的多个socket。(redis监听用户线程)

为啥 redis 单线程模型也能效率这么高?

  • 纯内存操作
  • 核心是基于非阻塞的 IO 多路复用机制
  • 单线程反而避免了多线程的频繁上下文切换问题

redis 主要有以下几种数据类型:

  • string
  • hash    
  • list            可以基于 list 实现高性能分页查询
  • set             两个人的粉丝列表整一个交集,看看俩人的共同好友是谁
  • sorted set      去重但可以排序,写进去的时候给一个分数,自动根据分数排序,可以做排行榜

redis 过期策略:

  • 定期删除:定期删除过期数据
  • 性删除:获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。

Redis的事务操作:

  • MULTI:标记事务的开始,redis将后续命令放入队列中,使用EXEC命令原子化地执行这个命令。
  • EXEC:在一个事务中执行所有先前放入队列的命令,然后回复正常的连接状态。
  • DIDCARD:清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
  • WATCH:当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的。
  • UNWATCH:清除所有先前为一个事务监控的键。
  • 使用方法:首先使用MULTI进入一个rendis事务,之后可以发送给多个redis命令,一点调用EXEC命令,那么redis就会执行事务中的所有命令。相反,调用DISCARD命令将会清空事务队列,然后退出事务
  • Redis使用WATCH命令实现事务的“检查再设置”(CAS)行为。
  1. 若redis没有incr命令
  2.             WATCH mykey
  3.             val = GET mykey
  4.             val = val + 1
  5.             MULTI
  6.             SET mykey $val
  7.             EXEC

由上面伪代码可知,如果存在竞争状态,并且有另外一个客户端在我们调用WATCH命令和EXEC命令之间的时间内修改了val变量的结果,那么事务将会运行失败。

redis内存淘汰机制:

  • noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。

redis 持久化的两种方式:

  • RDB:RDB 持久化机制,是对 redis 中的数据执行周期性的持久化。
  • AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中。

redis 原生支持的 redis 集群模式:

  • redis cluster:
  1. 自动将数据进行分片,每个 master 上放一部分数据
  2. 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的
  3. 在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西
  4. cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
  5. gossip 协议:ping,pong,meet,fail等
  • 分布式寻址算法:
  1. hash 算法(大量缓存重建)
  2. 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
  3. redis cluster 的 hash slot 算法
  • edis cluster 的高可用与主备切换原理:
  1. 判断节点宕机
  2. 从节点过滤
  3. 从节点选举

与哨兵比较:整个流程跟哨兵相比,非常类似,所以说,redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。

Redis 主从架构:redis replication、redis sentinel

  • redis replication:一个节点写,多个节点读
  • redis sentinel:一个节点写,多个节点读

缓存雪崩:

  • 缓存挂了,所有请求打到mysql上,数据库挂了
  • 缓存雪崩的事前事中事后的解决方案如下:
  1. 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
  2. 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
  3. 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

缓存穿透:

  • 黑客发出的 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
  • 解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。这样的话,下次便能走缓存了。

缓存与数据库的双写一致性

一、Cache Aside Pattern:

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后再删除缓存。

二、最初级的缓存不一致问题及解决方案:

  • 问题:先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
  • 先删除缓存,再修改数据库。如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。

三、比较复杂的数据不一致问题分析:

  • 更新操作:根据唯一标识路由到jvm内部队列->更新缓存->更新数据库
  • 查询操作:查询缓存(空)->根据唯一标识路由到jvm内部队列->等待更新操作完成->读取数据库/缓存
  • 队列中:
  1. 请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回
  2. 请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
  3. 优化:部署多个内存队列

详细方案:

  • 遇到这种情况,可以用队列的去解决这个问题,创建几个队列,如20个,根据商品的ID去做hash值,然后对队列个数取摸,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除。
  • 如果在更新的过程中,遇到查询场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,
  • 这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询操作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据,

高并发的场景下,该解决方案要注意的问题:

  • 多服务实例部署的请求路由:
  1. 可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上。
  2. 比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。
  • 热点商品的路由问题,导致请求的倾斜
  • 对于并发程度较高的,可采用异步队列的方式同步,可采用kafka等消息中间件处理消息生产和消费。
  1. 更新操作:RabbitMQ->更新缓存->更新数据库
  2. 查询操作:查询缓存(空)->RabbitMQ->等待更新操作完成->读取数据库/缓存

四、canal同步数据库:

redis 的并发竞争问题是什么?如何解决这个问题?了解 redis 事务的 CAS 方案吗?

  • 可以基于 zookeeper 实现分布式锁。
  • 你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。每次要写之前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据