redis缓存学习笔记、双写一致性
为什么要用缓存?
- 高性能、高并发
redis 和 memcached 有啥区别?
- redis支持复杂的数据结构
- redis原生支持集群模式, memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。
- redis 只使用单核,而 memcached 可以使用多核,每一个核上 redis 在存储小数据时比 memcached 性能更高。在 100k 以上的数据中,memcached 性能要高于 redis
redis 的线程模型:
- 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以redis才叫做单线程的模型。
- 文件事件处理器的结构包含 4 个部分:
- 多个 socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个socket可能会并发操作产生不同的操作,redis端IO多路复用程序(一个线程)监听多个server socket,将socket产生的事件放入队列中排队,事件分派器每次从队列中去一个事件,交给对应的事件处理器处理。
个人理解: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)行为。
- 若redis没有incr命令
- WATCH mykey
- val = GET mykey
- val = val + 1
- MULTI
- SET mykey $val
- 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:
- 自动将数据进行分片,每个 master 上放一部分数据
- 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的
- 在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西
- cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
- gossip 协议:ping,pong,meet,fail等
- 分布式寻址算法:
- hash 算法(大量缓存重建)
- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
- redis cluster 的 hash slot 算法
- edis cluster 的高可用与主备切换原理:
- 判断节点宕机
- 从节点过滤
- 从节点选举
与哨兵比较:整个流程跟哨兵相比,非常类似,所以说,redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。
Redis 主从架构:redis replication、redis sentinel
- redis replication:一个节点写,多个节点读
- redis sentinel:一个节点写,多个节点读
缓存雪崩:
- 缓存挂了,所有请求打到mysql上,数据库挂了
- 缓存雪崩的事前事中事后的解决方案如下:
- 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
- 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
缓存穿透:
- 黑客发出的 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
- 解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。这样的话,下次便能走缓存了。
缓存与数据库的双写一致性
一、Cache Aside Pattern:
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存。
二、最初级的缓存不一致问题及解决方案:
- 问题:先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
- 先删除缓存,再修改数据库。如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。
三、比较复杂的数据不一致问题分析:
- 更新操作:根据唯一标识路由到jvm内部队列->更新缓存->更新数据库
- 查询操作:查询缓存(空)->根据唯一标识路由到jvm内部队列->等待更新操作完成->读取数据库/缓存
- 队列中:
- 请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回
- 请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
- 优化:部署多个内存队列
详细方案:
- 遇到这种情况,可以用队列的去解决这个问题,创建几个队列,如20个,根据商品的ID去做hash值,然后对队列个数取摸,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除。
- 如果在更新的过程中,遇到查询场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,
- 这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询操作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据,
高并发的场景下,该解决方案要注意的问题:
- 多服务实例部署的请求路由:
- 可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上。
- 比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。
- 热点商品的路由问题,导致请求的倾斜
- 对于并发程度较高的,可采用异步队列的方式同步,可采用kafka等消息中间件处理消息生产和消费。
- 更新操作:RabbitMQ->更新缓存->更新数据库
- 查询操作:查询缓存(空)->RabbitMQ->等待更新操作完成->读取数据库/缓存
四、canal同步数据库:
- 打开sql的二进制文件,通过canal同步mysql数据到redis中
- 网上不错的方案:https://blog.kido.site/2018/12/09/db-and-cache-04/
redis 的并发竞争问题是什么?如何解决这个问题?了解 redis 事务的 CAS 方案吗?
- 可以基于 zookeeper 实现分布式锁。
- 你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。每次要写之前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据