内存数据库,缓存利器

介绍

开源ANSI C编写、支持网络、内存可持久化的日志型、KV数据库,提供多种语言API
单机10w
6379

优势

  • 高并发
  • 持久化
  • 丰富的数据结构
  • 高可用
  • 事务

    为什么快

  • 基于内存

  • 基于C
  • 数据结构简单,操作简单
  • 采用单线程/IO多路复用非阻塞IO

    线程模型:多路复用IO

  • IO多路复用

  • Reactor(多个套接字、IO多路复用程序、文件事件分派器、事件处理器)
  • 文件事件分派器队列消费单线程

  • read到很多才返回,为0会卡在那里,直到新数据来或者链接关闭

  • 写不会阻塞除非缓冲区满了
  • 非阻塞的IO提供了一个选项no_blocking读写都不会阻塞,读多少写多少,取决于内核的套接字字节分配
  • 非阻塞IO也有问题,线程要读数据,读了一点就返回,线程什么时候知道继续读?写一样
  • 一般都是select解决,但是性能低,现在都用epoll

Redis☆☆☆ - 图1

内存模型

  • 简单动态字符串SDS:键值的底层、AOF缓冲区、记录本身长度 C需要遍历、修改字符减少内存重新分配(空间预支配、惰性空间释放)、二进制安全(C只能保存文本数据,无法保存图片等二进制数据;SDS是使用长度去判断)、杜绝缓冲区溢出、兼容部分C字符串函数
  • 链表(保存多个客户端的状态信息、列表订阅发布、慢查询、监视器)
  • 字典(数据库、哈希键、Hash表节点、Hash冲突用单向链表解决、渐进式rehash-逐渐rehash新的键值对全部放到新hash表、每个字典带两个hash表-一个平时用,一个rehash时用)
  • 压缩列表
  • 整数集合
  • 跳跃表
  • 令牌
  • 漏桶

Redis☆☆☆ - 图2

应用场景

  • 热点缓存(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

    1. set name ljh
    2. get name
    3. getrange name 0 -1 截取字符串,0对应start1对应end
    4. getset name new_ljh 设置值,返回旧值
    5. mset key1 value1 key2 value2 批量设置
    6. mget key1 key2 批量获取
    7. setnx key value 不存在就插入(not exists
    8. setex key time value 过期时间(expire
    9. setrange key index value index开始替换value
    10. incr age 执行+1
    11. incrby age 10 执行+10
    12. decr age 执行-1
    13. decrby age 10 执行-10
    14. incrbyfloat 增减浮点数
    15. append 追加
    16. strlen 长度
    17. getbit/setbit/bitcount/bitop 位操作
  • hash

    1. hset myhash name ljh 添加一个键值对
    2. hget myhash name 取出值
    3. hmset myhash name ljh age 20 note "i am notes" 批量键值对
    4. hmget myhash name age note
    5. hgetall myhash 获取所有的键值对
    6. hexists myhash name 是否存在
    7. hsetnx myhash score 100 不存在的话创建,存在的话修改
    8. hincrby myhash id 2 增加2
    9. hdel myhash name 删除
    10. hkeys myhash 只取key
    11. hvals myhash 只取value
    12. hlen myhash 长度
  • list

    1. lpush mylist a b c 左插入
    2. rpush mylist x y z 右插入
    3. lrange mylist 0 -1 数据集合
    4. lpop mylist 弹出元素
    5. rpop mylist 弹出元素
    6. llen mylist 长度
    7. lrem mylist count value 根据值来删除(count表示要删除多少个值为value的元素)
    8. lindex mylist 2 指定索引的值
    9. lset mylist 2 n 索引设值
    10. ltrim mylist 1 2 截取指定的元素集合
    11. linsert mylist before 1 value 在值为1前面插入value
    12. linsert mylist after 1 value 在值为1的后面插入value
    13. rpoplpush list list2 list的最后一个元素移到list2
  • set

    1. sadd myset e 添加一个元素
    2. smembers myset 数据集合
    3. srem myset e 删除
    4. sismember myset e 判断元素是否在集合中
    5. scard key_name 获取set集合中元素个数
    6. sdiff | sinter | sunion 操作:集合间运算:差集 | 交集 | 并集
    7. srandmember 随机获取集合中的元素
    8. spop 从集合中弹出一个元素
  • zset

    1. zadd zset 1 one 添加一个元素(1表示score,用作排序使用)
    2. zadd zset 2 two
    3. zadd zset 3 three
    4. zincrby zset 1 one 分数+1
    5. zscore zset two 获取分数
    6. zrange zset 0 -1 获取全部的值
    7. zrange zset 0 -1 withscores 获取全部值并附带分数
    8. zrangebyscore zset 10 25 withscores 分数在某个范围的值
    9. zrangebyscore zset 10 25 withscores limit 1 2 分页
    10. Zrevrangebyscore zset 10 25 withscores 指定范围的值从大到小排序
    11. zcard zset 元素数量
    12. Zcount zset 获得指定分数范围内的元素个数
    13. Zrem zset one two 删除一个或多个元素
    14. Zremrangebyrank zset 0 1 按照排名范围删除元素
    15. 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还是淘汰时间

  • 缓存雪崩

    缓存集中时间失效

  1. - 加锁排队
  2. - 数据预热
  3. - 做二级缓存(一个长期,一个短期)
  4. - 过期时间随机值
  • 缓存击穿

    热点Key失效

- 用户鉴权
- 永不过期
- 互斥锁
- 布隆过滤器
  • 缓存穿透

    查询不存在的数据

- 缓存空对象(有缺陷)
- 布隆过滤器
  • 双写一致性
    更数更缓×:脏读、缓存更新太频繁
    删缓更数×:不采用给缓存设置过期时间策略,该数据永远都是脏数据
    延时双删策略√:
    (1)先淘汰缓存
    (2)再写数据库(这两步和原来一样)
    (3)休眠x毫秒(取决于业务处理或者主从同步时间),再次淘汰缓存
    先更数再删缓存√
    删除失败:放入消息队列延时重试,读binlog
  • 并发竞争
  • 大Key:某个key的value比较大
    查询:bigkeys/redis-rdb-tools
    删除:unlink(4.0以上)非阻塞删除/scan扫描删除(4.0以下)
  • 热点Key

    过期策略和淘汰机制

  • 过期策略

    • 定时删除:消耗内存-创建一个定时器,内存友好,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

  • 两个全局哈希表
  • rehash采用渐进式rehash(每次只移动一个链表)

    生成RDB会不会卡

    不会,因为采用fork bgsave子线程+cow(写实复制)

    重启

  • 先加载rdb

  • 然后重放增量aof(持久化开始到结束期间的)

    高可用

  • 持久化

  • 数据同步机制
  • 哨兵
  • 集群

    持久化机制

  • RDB:5分钟一次、冷备、恢复快、快照文件生成久耗CPU

    全量,快照,rdbsave、rdbload,性能好

  • AOF:appendOnly、数据齐全、回复慢文件大

    Append-only file,增量,写命令追加,更大更安全,都有优先

  • 数据初始化:从节点发送命令,主节点做bgsave,同时开启buffer

Redis☆☆☆ - 图3

数据同步机制

  • 主从同步:指令流、offset
  • 快照同步:RDB、缓冲区

    哨兵

  • 集群监控(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
  • 有隔离性、不保证原子性
  • 可以基于lua,保证一次性和顺序性;中间标记变量

    RedLock

  • 互斥访问

  • 避免死锁
  • 容错性

    限流

  • setnx ex

  • zset:窗口滑动、zset会越来越大
  • 令牌:定时push、然后leftpop、问题[空轮询、blpop、空连接异常、重试]
  • 漏桶funnel:make_space灌水之前调用漏水,腾出空间,解决于流水速率,Hash,原子性有问题
  • Redis-Cell

    底层数据结构

    SDS 简单动态字符串

  • 获取字符串长度O(1)

  • API安全,不会造成缓冲区溢出
  • 最多N次重分配(扩容used*2,大于1M扩容1M),惰性空间删除
  • 二进制安全,可以保存图片等
  • 保留C的\0以复用函数

    链表

  • 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,两个表都会查找,第一个表只减不增
  • murmurhash2算法(良好随机分布性/速度快)

    跳表 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):节约内存/灵活性,不支持降级
  • 添加新元素O(N)

    压缩列表(ziplist)

  • 实现:list(少量并且每个列表项是小整数值/较短字符串)、hash(少量且短),为了节约内存

  • zlbytes-4/zltail-4/zllen-2/entryX(previous_entry_length,encoding,content)-N/zlend-1
  • zllen可能是65535,需要遍历才能知道长度
  • 通过previous_entry_length从表尾向前遍历
  • 添加和删除可能造成连锁更新,O(N^2)

    对象系统

  • 引用计数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个
  • 有序集合对象

    • 跳表和字典的指针指向同一个元素,不会浪费内存
    • 元素个数<128且成员长度小于64字节

      数据库结构

  • redisServer(db[], dbnum默认为16,dirty,lastsave)

  • redisClient(db用户目标数据库)
  • redisDb(dict键空间,expires过期时间的字典)
  • flushdb
  • 读写:hit/miss/lru(OBJECT IDLETIME)/dirty(WATCH)/计数器+1触发持久化及复制/通知
  • 间隔性保存