说明:
本文章为书籍《Redis深度历险》的读书笔记,如有侵权,马上删除,不承担任何法律责任。
目录:
- 应用
- 原理
- 集群
- 拓展
- 源码
应用
内容列表:ZSET,分布式锁,HyperLogLog,布隆过滤器
1.ZSET 有序列表 sorted set
- 形式
- value 1 - score 1
- value 2 - score 2
- value 3 - score 3
- 数据结构
- value -> score : HashMap
- score -> value : SkipList
- 应用
- 粉丝列表:value是粉丝的用户id,score是关注时间,根据关注时间排序
- 学生成绩:value是学生的id,score是学生成绩,根据分数进行排名
- 延时队列:value是字符串,score消息到期时间;
- SKIPLIST 跳表
- 平均查找和插入 O(log n)
- 新插入元素层数
- 采用随机策略
- level1概率100%,level2概率50%,level3概率25%
2.分布式锁
- 使用场景
- 多进程/线程间需要串行执行操作
- 原理
- 使用redis键代表锁
- set表示占用锁,del表示释放锁
- setnx: set if not exists
- 方法1
- setnx -> do -> del
- 问题:do过程中出现异常,del未被执行,锁不会被释放
>
- 方法2
- setnx -> expire -> do -> del
- 问题:setnx后进程挂掉,expire未被执行,锁不会被释放
>
- 方法3
- setnx & expire (version 2.8) -> do -> del
- 问题1:do太久会导致锁被释放,多个进程进入临界区代码
- 问题2:del可能被其他进程执行
>
- 方法4
- 生成随机数val -> setnx & expire -> do -> del (验证key的val随机数是否一致)
- delifequals需要使用lua脚本,保证原子性
- 可以解决上面问题2,没有解决问题1
- 结论
- 方法3使用简单,如果执行时间不是太长,用方法3就好;可以加报警。
- 集群的问题:主的锁未同步到备之间,主挂了,会导致锁未被释放。
- 锁冲突解决方法:
- 直接抛异常,通知用户稍后再试
- sleep一会重试
- 请求转移至消息队列
3.HyperLogLog
- 应用:统计页面user view,需要除重
- 千万级别时set集合会浪费空间
- 记录低位连续零位的最大长度maxbit,可以估算出随机数的数量。
- 空间12KB,误差小于1%。
4.布隆过滤器bloom filter
- 应用:当查询某个row,可以先通过内存布隆过滤器滤掉大量不存在的row,再去查磁盘
- 其他:爬虫系统对url去重;垃圾邮件;
- 特征:当它说某个值存在时,这个值可能不存在;当它说不存在时肯定不存在;
- 上图中,如果命中了1/4/5,那么oracle大概率存在;因为这些可能是被其他key设置;
- 上图中,如果没命中1,那么oracle肯定不存在;
- 原理:
- h1, h2, h3为多个hash函数
- 当错误率为0.1%时,一个元素大约需要15bit
- 当不使用时,一个字符串可能就有几十个字节;set本身还有4或8字节的指针
- 所以布隆过滤器的空间优势还是非常明显的
原理
1.数据结构
- 总:为加速内存操作和空间使用做了很多优化
- string:
- sds(simple dynamic string)
- 带长度
- 内存预分配,加快扩容与缩容
- list
- 存储string列表
- 元素少时用ziplist,连续内存,无prev/next指针,但插入删除O(N)
- 元素多时用linkedList
- map
- murmurhash
- 元素少时用ziplist
- 元素多时用hashtable
- 扩容/缩容rehash:读先访问源表,写访问目标表
- set
- intset
- map的特殊形式
- sorted set
- 元素少时用ziplist
- 元素多时用skiplist+hashtable
2.单线程模型
- 为何快,benchmark的set可达10w/s
- 单线程模型(main thread,还有辅助thread),避免多线程上下文切换的问题
- 纯内存操作(almost)
- 非阻塞IO
- 阻塞IO:read(n) 表示有n个字节数据才返回
- 非阻塞IO:
- NIO: Non-blocking IO
- 读写方法不会阻塞,能读多少读多少
- 读方法和写方法都会通过返回值告知程序实际读写了多少字节
- 没有通知机制,通过os提供的事件轮询api来解决,epoll
- 管道
- mget,pipeline,合并多个网络来回
- 定时任务
- 大型hash表的渐进式迁移,过期key的主动淘汰,bgsave,bgaofrewrite
3.持久化
- RDB snapshot
- 全量模式
- 快照,内存数据的二进制序列化,存储紧凑
- fork子进程进行持久化;使用cow(copy on write)
- save 900 1
- save 300 10
- save 60 10000
- 满足上面任一触发bgsave,比如60秒内有至少10000key改变
- 性能更好
- AOF
- 增量模式
- append only file
- 连续增量备份,内存数据修改的指令记录文本
- appendfsync everysec [默认]
- appendfsync always
- appendfsync no
- 独立thread进行
- 比较
- rdb文件大,aof慢
- redis重启,先加载rdb内容,再增量重放aof日志。
集群
0.集群组建方式
- sharding: 客户端自己实现,扩缩容需要改代码(client->redis)
- proxy: 扩展性强,proxy增加耗时(client->proxy->redis)
- cluster: gossip协议,排查问题复杂(client->redis cluster)
1.Redis Sentinel (实现备份)
- master -> slave
- 正常:aof增量
- 新节点:先rdb,再aof
- sentinel
- 使用raft
- 可以看成是一个zookeeper的集群
- 负责持续监控主从节点的健康
- 当主节点挂,自动选择最优从节点为新主节点
- 客户端连接,先连接sentinel,查询主节点地址,在进行数据交换
2.Twemproxy (实现扩容)
- client -> {twemproxy} -> redis
- 优点:当业务增长时,无需修改client端代码,实现扩容。
- 缺点:
- 性能比直接连接损失大概20%
- 扩容时需要修改配置文件,停服务重启。
- 高可用:{twemproxy} 为多个twemproxy实例,以实现高可用,client需要感知多个twemproxy;
- node-ejection:打开时,请求可能到其他server;不打开,请求到同一个server,此时可使用slave+sentinel实现高可用。
- hash:可配置不同的hash算法。
3.Codis (实现扩容)
- 提升twemproxy扩容时的体验,实现平滑扩容,架构更复杂
- client -> codis -> redis
- key -> crc32 -> mod 1024 计算槽位 -> 对应redis实例 (1024个槽位)
- 集群配置中心使用zookeeper来实现
4.Redis Cluster (实现扩容)
- 没有Sentinel,没有proxy
- master节点对外提供读写服务,slave只提供读;每个都保存了集群的配置信息
- gossip协议
- 生产环境使用较少,出问题不好排查
- 作者思想来源于cpu的多核
拓展
1.stream: 小kafka
2.redlock
- 解决master挂,锁未同步至slave的问题
- 加锁时,向过半节点发送set指令,过半set成功即成功
- 释放锁时,向所有节点发送del指令
- 使用场景:redis集群;
- 需要考虑出错重试,时钟漂移等问题,复杂
3.过期策略
- 定期扫描
- 每10s一次
- 贪心策略
- 过期字典中随机选20个key
- 删除
- 如果过期key比例超过1/4,则重复
- 保证不能超过25ms
- 惰性策略
- 访问时检查是否过期,过期了则删除
- 大量key同时过期卡顿
- 将过期时间随机化
- 从节点
- 不进行定期扫描
- 主节点在key过期时,在aof文件中增加一条del
- 持久化
- 使用volatile-xxx过期策略
- 只会淘汰设置了过期时间的key
源码
nothing special