内存数据库,缓存利器
介绍
开源ANSI C编写、支持网络、内存可持久化的日志型、KV数据库,提供多种语言API
单机10w
6379
优势
- 高并发
- 持久化
- 丰富的数据结构
- 高可用
-
为什么快
基于内存
- 基于C
- 数据结构简单,操作简单
-
线程模型:多路复用IO
IO多路复用
- Reactor(多个套接字、IO多路复用程序、文件事件分派器、事件处理器)
文件事件分派器队列消费单线程
read到很多才返回,为0会卡在那里,直到新数据来或者链接关闭
- 写不会阻塞除非缓冲区满了
- 非阻塞的IO提供了一个选项no_blocking读写都不会阻塞,读多少写多少,取决于内核的套接字字节分配
- 非阻塞IO也有问题,线程要读数据,读了一点就返回,线程什么时候知道继续读?写一样
- 一般都是select解决,但是性能低,现在都用epoll
内存模型
- 简单动态字符串SDS:键值的底层、AOF缓冲区、记录本身长度 C需要遍历、修改字符减少内存重新分配(空间预支配、惰性空间释放)、二进制安全(C只能保存文本数据,无法保存图片等二进制数据;SDS是使用长度去判断)、杜绝缓冲区溢出、兼容部分C字符串函数
- 链表(保存多个客户端的状态信息、列表订阅发布、慢查询、监视器)
- 字典(数据库、哈希键、Hash表节点、Hash冲突用单向链表解决、渐进式rehash-逐渐rehash新的键值对全部放到新hash表、每个字典带两个hash表-一个平时用,一个rehash时用)
- 压缩列表
- 整数集合
- 跳跃表
- 令牌
- 漏桶
应用场景
- 热点缓存(set、hset)
- 限时业务(expire,验证码)
- 成绩、积分、排行榜(zzet:zadd、zrangebyscore)
- 分页/模糊搜索(zrangebylex)
- 计数器(incrby)
- 分布式会话(hset存储session)
- 分布式锁(setnx+expire)
- 社交网络(sismember)
- 最新列表(ltrim)
- 延时队列(时间做score,zrangebyscore)
- 消息队列(lpush、pub/sub)
位操作(setbit、getbit、bitcount)
缓存
- 排行榜(ZSet)
- 计数器/限速器(统计在线人数/浏览量/播放量)
- 分布式锁(setnx/del)
- 好友关系(点赞/共同好友)
- 简单消息队列(List/订阅/发布)
分布式Session
String :缓存,限流,计数器,分布式锁,分布式session
- Hash:存储用户信息,用户主页访问量,组合查询
- List:关注人时间轴列表
- Set:点赞,标签,好友关系
- Zset:排行榜
常用命令
- Keys
- setnx
- expire
- scan
set key value/get key/del key/exists key/type key/expire key 10/pexpire key 10/persist key/select index(index表示数据库编号,0/16)/flushdb/flushall/keys */
string
set name ljhget namegetrange name 0 -1 截取字符串,0对应start,1对应endgetset name new_ljh 设置值,返回旧值mset key1 value1 key2 value2 批量设置mget key1 key2 批量获取setnx key value 不存在就插入(not exists)setex key time value 过期时间(expire)setrange key index value 从index开始替换valueincr age 执行+1incrby age 10 执行+10decr age 执行-1decrby age 10 执行-10incrbyfloat 增减浮点数append 追加strlen 长度getbit/setbit/bitcount/bitop 位操作
hash
hset myhash name ljh 添加一个键值对hget myhash name 取出值hmset myhash name ljh age 20 note "i am notes" 批量键值对hmget myhash name age notehgetall myhash 获取所有的键值对hexists myhash name 是否存在hsetnx myhash score 100 不存在的话创建,存在的话修改hincrby myhash id 2 增加2hdel myhash name 删除hkeys myhash 只取keyhvals myhash 只取valuehlen myhash 长度
list
lpush mylist a b c 左插入rpush mylist x y z 右插入lrange mylist 0 -1 数据集合lpop mylist 弹出元素rpop mylist 弹出元素llen mylist 长度lrem mylist count value 根据值来删除(count表示要删除多少个值为value的元素)lindex mylist 2 指定索引的值lset mylist 2 n 索引设值ltrim mylist 1 2 截取指定的元素集合linsert mylist before 1 value 在值为1前面插入value值linsert mylist after 1 value 在值为1的后面插入value值rpoplpush list list2 将list的最后一个元素移到list2中
set
sadd myset e 添加一个元素smembers myset 数据集合srem myset e 删除sismember myset e 判断元素是否在集合中scard key_name 获取set集合中元素个数sdiff | sinter | sunion 操作:集合间运算:差集 | 交集 | 并集srandmember 随机获取集合中的元素spop 从集合中弹出一个元素
zset
zadd zset 1 one 添加一个元素(1表示score,用作排序使用)zadd zset 2 twozadd zset 3 threezincrby zset 1 one 分数+1zscore zset two 获取分数zrange zset 0 -1 获取全部的值zrange zset 0 -1 withscores 获取全部值并附带分数zrangebyscore zset 10 25 withscores 分数在某个范围的值zrangebyscore zset 10 25 withscores limit 1 2 分页Zrevrangebyscore zset 10 25 withscores 指定范围的值从大到小排序zcard zset 元素数量Zcount zset 获得指定分数范围内的元素个数Zrem zset one two 删除一个或多个元素Zremrangebyrank zset 0 1 按照排名范围删除元素Zremrangebyscore zset 0 1 按照分数范围删除元素
Jedis
数据结构
String:Binary-safe strings
- Hash
- Set
- ZSet/Sorted set:score、随即层数(只需要调整前后节点指针)、不止比较score还会比较value
- List:分页的坑
- Bitmap/Bit array
- HyperLogLog
- Stream
- Geo
- Pub/Sub
应用层
- string
简单字符串/XML/JSON/整数/浮点数二进制
支持:缓存、计数器、分布式Session、限速
底层:int、sds、embstr
- hash
稀疏类似nosql
支持:用户信息管理
底层:ziplist、hashtable
- set
支持:共同爱好
底层:intset、hashtable
- zset(Sorted set)
score排序
支持:排行榜
底层:ziplist(小)、hashtable+skiptable(大)
- list
最多2^32-1个元素,模拟栈/队列
命令:lpush/brpop
支持:阻塞队列
底层:ziplist+quicklist
- hyperloglog
- geo
- pub/sub
- bitmap
- stream
底层
- SDS
- 链表
- 字典
- 压缩列表
-
常见各类问题
缓存雪崩:加随机值、集群部署
- 缓存穿透:互斥锁、热点不失效
- 缓存击穿:BloomFilter
- 双写一致性:延时双删
- 并发竞争:分布式锁
- 大Key:bigkey命令,找到干掉;Redis4.0引入memory usage命令和lazyfree机制
- 热点Key:不失效、多级缓存、BloomFilter、读写分离、备份热key走不同机器
如何保证10w都是热点:最大内存(maxmemory)、淘汰策略、判断场景是加ttl还是淘汰时间
缓存雪崩
缓存集中时间失效
- 加锁排队- 数据预热- 做二级缓存(一个长期,一个短期)- 过期时间随机值
- 缓存击穿
热点Key失效
- 用户鉴权
- 永不过期
- 互斥锁
- 布隆过滤器
- 缓存穿透
查询不存在的数据
- 缓存空对象(有缺陷)
- 布隆过滤器
- 双写一致性
更数更缓×:脏读、缓存更新太频繁
删缓更数×:不采用给缓存设置过期时间策略,该数据永远都是脏数据
延时双删策略√:
(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠x毫秒(取决于业务处理或者主从同步时间),再次淘汰缓存
先更数再删缓存√
删除失败:放入消息队列延时重试,读binlog - 并发竞争
- 大Key:某个key的value比较大
查询:bigkeys/redis-rdb-tools
删除:unlink(4.0以上)非阻塞删除/scan扫描删除(4.0以下) -
过期策略和淘汰机制
过期策略
- 定时删除:消耗内存-创建一个定时器,内存友好,CPU不友好
- 惰性删除:可能存在大量key,内存不友好,CPU友好√
- 定期删除:检查、删除,但是是随机的,折中√
淘汰机制
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。这种策略使得我们可以向Redis提示哪些key更适合被eviction。
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。如果我们的应用对缓存的访问符合幂律分布(也就是存在相对热点数据),或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择allkeys-lru策略。
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。
- noeviction:禁止驱逐数据,当内存使用达到阈值的时候,所有引起申请内存的命令会报错
最佳实践
Master最好不要持久化,Slave开启aof,每秒1次
- Master和Slave最好在同一局域网
- 主从复制用单向链表,不用图
- 内存优化:尽可能用散列表
- 避免bigkey
- 使用lazy-free
- 尽可能用简单命令
- 数据量大,分批查询和删除
- 批量命令代替单个命令(mget/mset/hmget/hmset/pipline)
- 只使用db0
- 读写分离
-
如何hash
链式hash
- 两个全局哈希表
-
生成RDB会不会卡
不会,因为采用fork bgsave子线程+cow(写实复制)
重启
先加载rdb
-
高可用
持久化
- 数据同步机制
- 哨兵
-
持久化机制
RDB:5分钟一次、冷备、恢复快、快照文件生成久耗CPU
全量,快照,rdbsave、rdbload,性能好
AOF:appendOnly、数据齐全、回复慢文件大
Append-only file,增量,写命令追加,更大更安全,都有优先
数据初始化:从节点发送命令,主节点做bgsave,同时开启buffer
数据同步机制
- 主从同步:指令流、offset
-
哨兵
集群监控(Monitoring):Sentinel不断的去检查你的主从实例是否按照预期在工作。
- 消息通知(Notification):Sentinel可以通过一个api来通知系统管理员或者另外的应用程序,被监控的Redis实例有一些问题。
- 自动故障转移(Automatic failover):如果一个主节点没有按照预期工作,Sentinel会开始故障转移过程,把一个从节点提升为主节点,并重新配置其他的从节点使用新的主节点,使用Redis服务的应用程序在连接的时候也被通知新的地址。
- 配置提供者/配置中心(Configuration provider):Sentinel给客户端的服务发现提供来源:对于一个给定的服务,客户端连接到Sentinels来寻找当前主节点的地址。当故障转移发生的时候,Sentinels将报告新的地址。
脑裂
监控
- 故障转移
-
集群(Cluster)
16384个slots,每个节点负责一部分slots,CRC16(key)%16384
- 通过gossip协议交互集群信息,保存其他节点slots分配信息
解决大数据量存储导致的各种慢问题
链表
- 多主:横向扩容
-
备用方案
主从+哨兵+cluster
- ecache+Hystix+降级+熔断+隔离
-
事务
命令:MULTI-EXEC-DISCARD
不支持回滚:因为Redis命令只在语法、类型等编程错误的时候才出错,这个应该在上层解决
MULTI(事务开始)->命令入队->执行EXEC
- WATCH
- 有隔离性、不保证原子性
-
RedLock
互斥访问
- 避免死锁
-
限流
setnx ex
- zset:窗口滑动、zset会越来越大
- 令牌:定时push、然后leftpop、问题[空轮询、blpop、空连接异常、重试]
- 漏桶funnel:make_space灌水之前调用漏水,腾出空间,解决于流水速率,Hash,原子性有问题
-
底层数据结构
SDS 简单动态字符串
获取字符串长度O(1)
- API安全,不会造成缓冲区溢出
- 最多N次重分配(扩容used*2,大于1M扩容1M),惰性空间删除
- 二进制安全,可以保存图片等
-
链表
list(元素较多时/元素都是较长字符串)、pub/sub、慢查询、监视器、Redis服务器保存多个客户端的状态信息、构建客户端输出缓冲区
- list(listNode) 无环双向链表
-
字典
实现:Redis数据库、hash(元素较多时)
- dict(dictht(dictEntry *table))
- 链地址法
- rehash每个dict含有两个dictht,采用头插法(考虑速度)
- rehash扩容:dictht[0].used*2后取最近的2^n的值作为新dictht的size
- rehash扩容时机:is bgsave/bgrewriteaof & factor >= 5 || isNot bgsave/bgrewriteaof & factor >= 1
- rehash缩容时机:factor < 0.1
- 渐进式rehash,两个表都会查找,第一个表只减不增
-
跳表 skiplist
实现:Sorted Set(元素较多时)、集群节点中用作内部数据结构
- 平均O(logN),最坏O(N)
- 大部分情况下替代平衡树
- zskiplist(zskiplistNode(Level[forward,span],backward,score,obj))
- 幂次定律
成员变量(SDS)唯一,分值不需要唯一(按照成员变量字典序)
整数集合(intset)
实现:set(元素不多)
- 从小到大不重复
- intset(encoding,length,contents[])
- upgrade(16->32->64):节约内存/灵活性,不支持降级
-
压缩列表(ziplist)
实现:list(少量并且每个列表项是小整数值/较短字符串)、hash(少量且短),为了节约内存
- zlbytes-4/zltail-4/zllen-2/entryX(previous_entry_length,encoding,content)-N/zlend-1
- zllen可能是65535,需要遍历才能知道长度
- 通过previous_entry_length从表尾向前遍历
-
对象系统
引用计数refcount:垃圾回收、对象共享(仅对包含整数值的字符串对象)
- 生命周期:创建、操作、释放
- 访问时间记录信息:空转时长,大的优先删除
- 键对象和值对象
- redisObject(type,encoding,ptr)
- type:STRING,LIST,HASH,SET,ZSET
- encoding[OBJECT ENCODING命令可以查看]:
- STRING:int(long类型整数),embstr(embstr编码的sds),raw(sds)
- LIST:ziplist(压缩列表),linkedlist(双端链表)
- HASH:ziplist(压缩列表),ht(字典)
- SET:intset(整数集合),ht(字典)
- ZSET:ziplist(压缩列表),skiplist(跳跃表和字典->zscore)
- 字符串对象
- embstr:短字符串,redisObject和sdshdr连续空间一次分配,只读,修改后就会变成raw
- long、double是转成string存储的
- 列表对象
- 所有字符串元素长度都小于64字节且元素数量小于512个用ziplist
- linkedlist嵌套字符串对象
- 哈希对象
- 所有键值字符串长度小于64字节且数量少于512个
- 集合对象
- 所有元素都是整数值且元素数量<=512个
有序集合对象
redisServer(db[], dbnum默认为16,dirty,lastsave)
- redisClient(db用户目标数据库)
- redisDb(dict键空间,expires过期时间的字典)
- flushdb
- 读写:hit/miss/lru(OBJECT IDLETIME)/dirty(WATCH)/计数器+1触发持久化及复制/通知
- 间隔性保存
