1.简介
高可用,高性能 吞吐量能达到10W级别的nosql缓存中间件。
为什么单线程还那么快?
因为他底层用的是单线程io多路复用,也就是说 多个io请求 复用 一个线程,主线程 会把多个io放进一个任务队列,由一个任务分发器分发给工作线程进行io,这样批量处理io请求,所以它的性能是很快的
2.数据结构 以及应用场景
String 存储字符串 计数,提供了原子性操作
map 存储对象 session
list 集合 队列 列表(先进先出) 文章列表 好友列表
set 不可重复无序,可以统计点赞列表、关注列表,网站ip访问量
zset 有序不可重复 , 加了个分数 用来排序, 排行榜
zskiplist 跳跃表:多层级链表,写入时随机获取层级,高层级的数据也会写入低层级,查询时会从高层级开始查询,没有查到就降低层级从该节点继续查询,提高查询效率,空间换时间
bitmap 数据结构就是内存中连续的二进制位(bit)并且每个bit位初始值都是0,数据hash后的值转成bit位 存在在bit 用户签到,布隆过滤器: 通过key算bit的占位是否存在,如果bit没有命中那么这个key就没有保存过,如果命中 则说明 这个key 有可能被保存过
布隆过滤器
hyperloglog 不精确统计 统计uv浏览量
1.内部结构是有固定的16384个桶
2.通过对值进行hash 选取低14位 计算出 选的哪个桶
3.再通过15位开始数0的个数,这个个数 大于 桶当前的值 就更新为桶的值 返回1
UV浏览量统计
ziplist
高级用法:
1.pipeline 批量执行命令,减少网络开销
2.lua脚本,串行执行一系列命令,锁,秒杀场景 nginx + lua 直接访问redis(库存获取 和 库存扣减)
3.事务:最后一个功能是事务,但 Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。
3.高可用 架构
1.单例
2.主从
3.主从+sentinel哨兵
4.cluster集群 多master节点,集群slave冗余机制:防止master 和 slave 都挂掉,没有slave切换
4.主从复制 + 心跳 + 持久化
matser 10S一次心跳,slave 1S一次心跳
主从复制是异步的:
1.首次由slave发起psync命令给master
2.master收到消息,根据本地offset 发送消息给slave
3.首次请求会触发全量复制(fork进程来拷贝数据,引起性能抖动),后续就是增量复制(由master 主动发起异步复制)
4.复制超时时间默认60S 超时会认为复制失败
持久化:
rdb快照:将内存数据生成快照替换旧的快照文件,触发机制 60 300 900s,10000 10 1 操作,有可能造成数据丢失(不能优先作为数据恢复方案),持久化fork紫禁城来io操作如果数据文件太大会导致客户端服务暂停,恢复数据速度快,数据文件小
优化快照生成影响性能:控制redis实例内存大小10G以内,部署多master
aof文件追加:每次操作就追加一条操作日志,先写缓存中每秒刷入日志文件,顺序写性能好,丢失数据比较少,但数据文件大并且写入性能会比rdb差,数据恢复慢
rewrite机制:清除冗余操作日志,保存最后一条操作记录即可,文件太大就会触发,不让他频繁触发 可以配置增量size或者百分比
两种一起使用,rdb每小时 每天 做数据备份发送云端,aof保证数据不丢失,如果aof文件丢失或者损坏 可以用fix命令修复,无法使用就用rdb备份
5.sentinel 机制 (sentinel高可用 和 选举)
sdown :主观宕机, 如果一个哨兵ping一个master(每秒一次),超过了down-after-milliseconds指定的毫秒数之后,就主观认为master宕机
odown :客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机
qourum机制:大于半数的sentinel实例都判断为宕机 才可以认定为宕机,所以sentinel数量要3台
majority机制:必须要有超过半数都运行选举了,才可以选举master,会由某个哨兵了选一个slave进行主备切换
6.cluster 机制 (扩容、集群部署 树状 、slave冗余)
宕机判断:节点内部会彼此互发心跳,半数以上通过就认定节点宕机
树状 部署主从 避免引起大量主从复制 占用带宽
集群slave冗余:保证集群高可用
7.LRU机制
noeviction(不驱逐):当内存使用超过配置的时候会返回错误,不会驱逐任何键
allkeys-lru(最久没使用):加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
volatile-lru(过期中最久没使用):加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
allkeys-random(随机):加入键的时候如果过限,从所有key随机删除
volatile-random(过期中随机):加入键的时候如果过限,从过期键的集合中随机驱逐
volatile-ttl(马上过期):从配置了过期时间的键中驱逐马上就要过期的键
volatile-lfu(过期中最少使用):从所有配置了过期时间的键中驱逐使用频率最少的键
allkeys-lfu(最少使用):从所有键中驱逐使用频率最少的键
8.性能优化
1.多master
2.fork进程(rdb生成,rewrite),一般控制redis的内存在10GB以内
3.主从复制风暴问题 (树状结构)带宽占用严重和内存空间
4.最大打开文件句柄
5.Master部署要用树状不要用星状
6.保持服务器空闲内存,持久化(rewrite) 和 同步 会消耗 内存 和 cpu 资源,引起性能抖动
7.避免在主库压力大 加过多从库,因为要同步
9.数据恢复、备份、迁移方案
备份:
(1)写crontab定时调度脚本去做数据备份
(2)每小时都copy一份rdb的备份,到一个目录中去,仅仅保留最近48小时的备份
(3)每天都保留一份当日的rdb的备份,到一个目录中去,仅仅保留最近1个月的备份
(4)每次copy备份的时候,都把太旧的备份给删了
(5)每天晚上将当前服务器上所有的数据备份,发送一份到远程的云服务上去
恢复:
(1)默认直接基于AOF日志文件恢复数据,如果AOF文件破损,那么用redis-check-aof fix 恢复
(2)如果redis当前最新的AOF和RDB文件出现了丢失/损坏,那么可以尝试基于该机器上当前的某个最新的RDB数据副本进行数据恢复
(3)如果你同时开启aop 和 rdb ,用RDB副本恢复会失败,因为不会进行rdb数据文件恢复,并且每次重启都会选择aof文件恢复, 所以 必须关掉aof后再重启,这样就会用rdb文件恢复,然后再用热修改 config set 开启aof。
迁移:https://www.cnblogs.com/xingxia/p/redis_migration_tool.html
1.主从进行全同步期间,如果主库此时有expire 命令,那么到从库中,该命令将会被延迟执行 (过期时间不一致)
解决:1.用 expireat 做过期时间 2.对过期时间不敏感 其实也可以不用处理
expire/pexpire/setex/psetex 命令在复制到从库的时候转换成时间戳的方式,比如expire 转成expireat命令,setex转换成set和expireat命令
2.使用Redis-Shake迁移数据 https://www.cnblogs.com/caoweixiong/p/14239315.html
配置Redis-Shake(源集群和目标集群的Master节点和IP)
在线迁移(实时同步数据)
命令 :./redis-shake -type sync -conf redis-shake.conftail -1000f /var/log/redis-shake.log
离线迁移(备份文件导入)
导出RDB文件
./redis-shake.linux -type dump -conf redis-shake.conf
、使用以下命令把RDB文件导入到目标集群
./redis-shake.linux -type restore -conf redis-shake.conf
迁移后验证
通过info命令查看Keyspace中的Key数量 (info keyspace),确认数据是否完整导入。
检查对比源端集群和目标集群中的每一个master节点的keys数量是否一致。
如果数据不完整,可使用flushall或者flushdb命令清理实例中的缓存数据后重新同步。
3.move迁移(move迁移有个弊端就是会删除源库的所有key迁移到目标库中)
4.migrate迁移 migrate迁移不会删除原有的key并且迁移到目标库中
3 和 4 都是离线 模式下 迁移
5.通过一个自建的redis 做中间 转移 然后 在迁移到 最终的 redis上
6.如果是阿里云上的redis 是有迁移工具 Redis-Shake
7.执行 save\bgsave 触发数据持久化 RDB文件
拷贝redis备份文件(dump.rdb)到目标机器
重启目标实例重新load RDB 文件
redis-full-check校验数据
10.主从不一致 1.强制读主(选择性部分业务)
https://www.yuque.com/jdxj/3456fg/imsh2t
https://blog.youkuaiyun.com/qq_41373681/article/details/107605756
第一个最普通的实现方式,就是在 Redis 里使用 SET key value [EX seconds] [PX milliseconds] NX
创建一个 key,这样就算加锁。其中:
NX
:表示只有key
不存在的时候才会设置成功,如果此时 redis 中存在这个key
,那么设置失败,返回nil
。EX seconds
:设置key
的过期时间,精确到秒级。意思是seconds
秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。PX milliseconds
:同样是设置key
的过期时间,精确到毫秒级。- 比如执行以下命令:
-
SET resource_name my_random_value PX 30000 NX
普通锁的问题:
1.为啥要用 random_value
随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,比如说超过了 30s,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除 key 的话会有问题,所以得用随机值加上面的 lua
脚本来释放锁。
2.因为如果是普通的 Redis 单实例,那就是单点故障。或者是 Redis 普通主从,那 Redis 主从异步复制,如果主节点挂了(key 就没有了),key 还没同步到从节点,此时从节点切换为主节点,别人就可以 set key,从而拿到锁。
RedLock 算法
这个场景是假设有一个 Redis cluster,有 5 个 Redis master 实例。然后执行如下步骤获取一把锁:
- 获取当前时间戳,单位是毫秒;
- 跟上面类似,轮流尝试在每个 master 节点上创建锁,超时时间较短,一般就几十毫秒(客户端为了获取锁而使用的超时时间比自动释放锁的总时间要小。例如,如果自动释放时间是 10 秒,那么超时时间可能在
5~50
毫秒范围内); - 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点
n / 2 + 1
; - 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
- 要是锁建立失败了,那么就依次之前建立过的锁删除;
- 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。
因为redis自己原理机制是主从异步复制所以比较麻烦 还要基于算法
zookeeeper 是强一致性 所以就没有那么多问题 ,需要复杂的机制去实现,逻辑更加简单 健壮
zk 分布式锁
zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。
有两种实现方式:
1.全部监听锁节点,以前比较早期都是这么实现,监听的请求太多 会引起惊群效应,会并发争抢,性能比较低
2.按节点大小顺序监听排在自己前面的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 ZooKeeper 给通知
但是,使用 zk 临时节点会存在另一个问题:由于 zk 依靠 session 定期的心跳来维持客户端,如果客户端进入长时间的 GC,可能会导致 zk 认为客户端宕机而释放锁,让其他的客户端获取锁,但是客户端在 GC 恢复后,会认为自己还持有锁,从而可能出现多个客户端同时获取到锁的情形。
redis 分布式锁和 zk 分布式锁的对比
- redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
- zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
另外一点就是,如果是 Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。
Redis 分布式锁大家没发现好麻烦吗?遍历上锁,计算时间等等......zk 的分布式锁语义清晰实现简单。
Redis 跳跃表(zset 数据结构)
是有序的,所以需要一个hash结构来存储value和score的对应关系,另一方面需要提供按照score来排序的功能,还能够指定score的范围来获取value列表的功能,上述也就是跳跃表要实现的功能
- zskiplist
- 一个跳跃表由多个跳跃表节点组成
- header:跳跃表的表头节点
- tail:指向跳跃表的表尾节点
- level:记录目前跳跃表内,层数最大的那个节点的层数比如tail的层数为L5
- length:记录目前跳跃表的长度,目前3个跳跃表节点则length=3
- zskiplistNode
- Level[] :标记L1 L2 L3 L4标记节点的各个层
- BW:backward 后退指针,通过tail遍历bw为空为止即为header
- 前进指针:level[i].forward 从表头向表尾方向访问节点
- score:各个跳跃表中1.0 2.0 3.0,即通过查找时层的跨度,比如从L5层通过前驱指正找到尾节点跨度为3
- obj:成员变量,存储是一个指针,指向一个字符串对象,而字符串对象保存的是一个SDS值
总结: 空间换时间
1.多层级的链表结构存储数据,随机层级存储,高层级的数据 底层级的链表也会保存
2.查询以高层级链表先查,没有查询到根据高层级的节点 再往 下一层级的那个节点继续查询
场景:
- 延时队列:当队列中消费失败后放入到延时队列中
- 分页排序:比如拉取消息时根据上一次的消息id来进行拉取
- 时间磋增量查询:根据时间磋来判断是否有新的消息
Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,跳跃表按score从小到大保存所有集合元素。使用跳跃表的结构可以获得比较高的查找效率
HyperLogLog
1.内部结构是有固定的16384个桶
2.通过对值进行hash 选取低14位 计算出 选的哪个桶
3.再通过15位开始数0的个数,这个个数 大于 桶当前的值 就更新为桶的值 返回1
UV浏览量统计
Bitmap
通过key算bit的占位是否存在,如果bit没有命中那么这个key就没有保存过,如果命中 则说明 这个key 有可能被保存过
布隆过滤器
热点key
1.将对于热点请求的数据进行,一主多从+哨兵的redis集群架构,有多个从主机负责处理读请求。 读写分离
2在redisCluster集群架构下,利用flink,storm这样的实时计算技术,对这SKU进行热点感知,只要是热点品就将数据同步给多台master,并改变lua脚本的执行请求路由的策略。
缓存穿透 穿过去了-- 缓存中 没有这个key
1.设置空值
1.1升级 然后在去数据库查询出值 在更新回去
2.常用方案 布隆过滤器(key 用hash函数 算的哈希值 就是指向bit的位置 bit位默认是0 被指向后更新成1)
(如果布隆过滤器判断元素在集合中存在, 不一定存在.,如果布隆过滤器判断不存在, 则一定不存在.) guava实现,redis Bitmaps 实现
缓存击穿 高并发访问的时候突然 key 失效
访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间
1.解决方式也很简单,可以将热点数据设置为永远不过期;
2.针对个别热点key ,如果查询缓存不存在就基于 redis or zookeeper 实现互斥锁互斥锁 setnx,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。阻塞的线程 休眠50毫秒后 重写获取数据(获取数据空 再尝试 获取锁 )
3.保存热点key到本地内存
4.缓存过期监听 redis自带功能
5.二级缓存 缓存过期时间和 一级缓存过期时间错开
6.定时任务 缓存重建 或者 重置过期时间
缓存 db 一致性方案
1.访问key 都加锁
2. 更新db 在更新缓存 问题: 缓存更新失败 数据不一致
3. 双删: 先删除缓存 更新db 在删除或者更新缓存 问题:存在并发问题 的数据不一致
4.缓存过期机制: 更新db,设置缓存过期时间
5.canal 监听 binlog 根据binlog 去更新 缓存
6.本地队列 所以操作 串行执行
缓存雪崩解决方案
1、事前解决方案 (吞吐不够,集群来凑)
吞吐不够被打崩:集群Redis Cluster 分布式存储缓存 加节点, 双机房
2、事中解决方案 (多级缓存 + hystrix 限流 降级(可以用队列和发布订阅,通知系统 设置本地缓存))
ecache 二级缓存
redis 限流 降级(独立的缓存服务) hystrix 线程隔离 保护其他业务的redis 正常, 线程不被占用, tomcat 没有资源 接收 服务 导致 整个服务挂掉
针对热点key 做限流 令牌桶 或者 漏桶
3、事后解决方案(数据恢复和预热)
(1)redis数据可以恢复,做了备份,redis数据备份和恢复,redis重新启动起来
(2)redis数据彻底丢失了,或者数据过旧,快速缓存预热,redis重新启动起来