第一章说了, redis这类db的出现, 是为了解决访问数据库过慢的问题
数据库慢就是两种情况:1. 读慢, 2. 写慢
读
上图是最基本的redis读数据流程
但是这时候会出现一个问题, 设置缓存数据以后, val如果不删除, client会一直会读取缓存, 所以这时候会加入一个set timeout的操作
这样做表面上解决了大部分情况的读取速度的情况, 但是又有3种并发情况:
- 击穿: 当ttl过期的时候,同一时间,有一个超大的并发向数据库进行请求。
为了解决这个问题, 引入了 set nx(相当于排他锁) 和 sleep, 像下面这样子
这个方案还是存在两个问题
- 【client2】sleep 究竟多久合适?时间长了,请求处理不及时。
- 【client1】del look 之前 因为某些不可抗力出现问题了。导致【client2】出现死锁。
解决方案:
- 根据实际 select 时间,来规划 sleep 时间
- 增加 look的ttl 来防止死锁, 并增加线程,来延长ttl时间。 如下图:
- 雪崩:大量的key缓存出现过期, 导致大量请求跑到数据库了(击穿的加强版)
这里要分两种情况:
- 缓存数据跟时间没有关系。【解决方案就是 随机ttl, 让缓存时间均匀分布】
- 缓存时间跟时间有关系,大量缓存再某一时间需要更新。 比如: 每个月都会更新一次信用卡账单,这时候上个月的数据就需要丢弃
解决方案也分两种:
- 再已知数据的情况下,可以使用预加载, 提前缓存好数据【比如 查询 上个月的信用卡账单,这个月2号才能查, 1号用来计算和缓存数据 】
- 在未知数据情况下,【 还是查账单, 上个月账单, 这个月1号0点开始就能查, 然后有大量请求出现】这种情况下必须采用 击穿的方案, 并在客户端进行优化, 过滤一部分客户端, 先等待或者返回等待提示。
- 穿透:查询db中不存在的值。
解决方案分两个步骤:
- 如果遇到不存在的值, 设置一个特殊标记作为缓存的值, 表示不存在, 不重复查数据库
- 采用过滤器来减少出发。 常见的有 Bloom Filter 布隆过滤器, Counting Bloom Filter, 布谷鸟过滤器【留坑】
第一步通过 过滤器 查询key, 返回true,则执行第三步获取key的值。第五步查询key2 返回false直接结束, 不再执行获取第三步的操作。
过滤器的判断是通过客户端设置的,一般再插入数据库的时候, 就需要设置过滤器了。这里有一个双写的坑存在,但是我觉得是完全可以接受的,毕竟这个是通过概率来过滤请求的,数据之间还有碰撞存在【即使过滤器返回true, 数据库中也不一定存在】(如果担心设置的时候出问题, 可以订阅binglog日志, 通过数据库的同步, 来更新)
写
写的方法有以下几种
- 正常的命令调用 【最常用的】
- pipe 管道式调用
cat data.txt | redis-cli --pipe
【这个用的非常少,一般用于冷启动】
- 队列[list],集合[set/sorted set],发布订阅[pub/sub] 【异步】
发布订阅是有一个坑的, 以下是场景模拟
- A 再A1这个频道发布了一个“aaa”这个消息,这时候并没有任何订阅者
- B 这时候订阅了A1这个频道。“aaa”这个消息 B是收不到的
- B 一段时间后挂了, 挂了到修复这段时间的消息, B也是收不到的, 所以会丢数据
- 事务【数据一致性,但并不完整】
redis的事务分两种情况
- multi是先把命令丢到一个队列里面, 在exec执行的时候, 在把队列里的命令丢进去
- 带上watch之后, 一旦aaa的值发生改变, exec直接失败, 后续客户端自己处理错误逻辑
回收策略
- noeviction 返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令
- allkeys-lru 淘汰最后访问时间最久的元素(很久没访问的数据),使得新添加的数据有空间存放
- volatile-lru 淘汰最后访问时间最久的元素,但仅限于在过期集合的键,使得新添加的数据有空间存放
- allkeys-random 回收随机的键使得新添加的数据有空间存放
- volatile-random 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键
- volatile-ttl 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放
- volatile-lfu 淘汰最近访问频率最小的元素(访问次数最少的数据),使得新添加的数据有空间存放
- allkeys-lfu 淘汰最近访问频率最小的元素,但仅限于在过期集合的键,使得新添加的数据有空间存放
LFU 缺点:
- 最新加入的数据常常会被踢除,因为其起始方法次数少。
- 如果频率时间度量是1小时,则平均一天每个小时内的访问频率1000的热点数据可能会被2个小时的一段时间内的访问频率是1001的数据剔除掉
LRU 缺点:- 可能会由于一次冷数据的批量查询而误导大量热点的数据。
redis 过期原理:
- 【被动】 在访问的时候,把过期的key删除
- 【主动】 周期轮询采样key(增量),在牺牲一定的内存下,保证性能
持久化
一般存储层, 有两种方式做持久层
- 快照(RDB)
快照的方式有两种:
- 阻塞【save】
阻塞的情况下, redis 不能对外提供服务, 这种情况下一般场景是不可以接受的, 主要应用于维护
- 非阻塞【bgsave】
非阻塞的首先要解决一个问题:8点的时候开始备份, 备份时间需要半小时, 但是这半小时之间数据会发生变化,但是备份的数据要是8点这个时间的数据, 不允许八点以后数据的变化影响到备份的数据
这里的解决方案是 copy-on-write(写入时复制)
原理如下:
如果主进程修改数据,则新建内存,指向新的地址
触发条件
配置文件中
- 缺点
- 不支持拉链, 只有一个 dump.rdb 文件
- 丢失数据比较多
- 日志(AOF)
相当于 binglog, 恢复的时候会触发io操作
触发条件
优点:就是每一个操作都会记录下,丢数据就比rdb好很多
缺点:由于运行非常久以后, AOF日志会变得非常庞大。
针对上述缺点,redis 有两种解决方案
- 在 4.0 以前,合并一些命令,日志结构没有变化,但随着时间越来越久,日志文件还是越来越大
- 在 4.0 以后,采用 RDB + AOF的方式来保存这个日志文件, 也就是下面所说的第三点。
上述方案可通过BGREWRITEAOF
来执行
- RDB + AOF
镜像 + AOF日志 混合。将老的命令合并成RDB格式到AOF文件, 然后以增量的方式以存放AOF格式的命令到AOF文件