[学习笔记] - Redis

Redis是完全开源免费的,遵守BSD协议,是一个高性能的key - value的Nosql数据库。Redis与其他key - value缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
  • Redis支持数据的备份,即master-slave模式的数据备份

优点

  • 性能极高
  • 丰富的数据类型
  • 原子性
  • 丰富的特性
  • 高速读写

缺点:

  • 持久化
    • 每次写入全部数据,代价很高
    • 回复速度慢
  • 耗内存

常用使用场景

  • 缓存
  • 排行榜
  • 计数器
  • 分布式会话
  • 分布式锁
  • 社交网络
  • 最新列表
  • 消息系统

内存淘汰机制

  • volatile-lru:设定超时时间的数据中,删除最不常用的数据
  • allkeys-lru:通过LRU算法删除最久没有使用的键
  • volatile-random:从过期键的集合中随机驱逐
  • allkeys-random:从所有key随机删除
  • volatile-ttl: 从配置了过期时间的键中驱逐马上就要过期的键
  • volatile-lfu: 从所有配置了过期时间的键中驱逐使用频率最少的键
  • allkeys-lfu: 从所有键中驱逐使用频率最少的键
  • noeviction: 如果缓存数据超过了maxmemory限定值,并且客户端正在执行的命令(大部分的写入指令,但DEL和几个指令例外)会导致内存分配,则向客户端返回错误响应

过期时间使用常见

  • 限时的优惠活动信息
  • 网站数据缓存
  • 验证码
  • 限制访问频率

Redis命令

就像MySql一样,Redis通过客户端执行命令

  • keys *: 返回所有的key信息
  • exists KEY: 是否存在key
  • expire KEY: 设置key过期时间
  • del KEY: 删除key
  • persist KEY: 取消时间
  • ttl KEY: key还有多少生存时间
  • type KEY: 返回key的类型
  • select INDEX: 选择数据库
  • move KEY INDEX: 转移key到index数据库
  • randomkey: 返回一个随机key
  • rename KEY KEY2: 重命名key
  • echo: 打印命令
  • dbsize: 数据库key数量
  • info: 数据库信息
  • config get *: 获取配置信息
  • flushdb: 清空当前数据库
  • flushall: 清空所有数据库

Redis命名规范

Redis命名没有长度限制

  • key不要超过1024字节
  • key不要太短,可读性会降低
  • 在一个项目使用同一的命名模式
  • key名称区分大小写

用冒号的方式来分组:user:id:name

数据类型

字符串

  • String: 字符串
    • 最大储存512MB
    • 二进制安全的
  • 命令
    • set KEY VALUE: 赋值key,覆盖旧值
    • setnx KEY VALUE:
      • 如果key不存在,则设置返回1
      • 如果key存在,则不设置返回0
    • get KEY: 获取值,不存在返回nil,如类型错误,则返回一个错误
    • getrange KEY START END: 获取key指定范围的值
    • getset KEY VALUE: 更新并返回旧值
    • strlen KEY: 获取值的长度
    • del KEY: 删除key
    • mset KEY1 VALUE1 KEY2 VALUE2: 批量写入
    • mget KEY1 KEY2: 批量读
    • incr KEY: 自增
      • 将key的值+1,若不存在则初始化为0,然后+1
    • incrby KEY NUM: 自增并设置增量值
    • decr KEY: 自减
      • 将key的值-1,若不存在则初始化为0,然后-1
    • decrby KEY NUM: 自减并设置减量值
    • append KEY VALUE: 新增写入(拼接)

字符串原理

Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组)。而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将 SDS 用作 Redis 的默认字符串表示。

1
2
3
4
5
6
7
8
9
10
11
12
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;

// 记录 buf 数组中未使用字节的数量
int free;

// 字节数组,用于保存字符串
char buf[];

};
  • 保存空字符的1字节空间不计算在SDS的len属性里面
  • 遵循空字符结尾这一惯例的好处是,SDS可以直接重用一部分C字符串函数库里面的函数。
  • 不要像C一样为了获取长度遍历整个字符串,知道长度也可以防止缓冲区溢出
  • 通过未使用空间,减少修改字符串时带来的内存重分配次数
    • 空间预分配:分配修改所必须要的空间,还会为SDS分配额外的未使用空间
    • 惰性空间释放:不立即使用内存重分配来回收缩短后多出来的字节,而是用作后续使用并等待将来使用
  • 二进制安全
  • free
    • 0:这个SDS没有分配任何未使用空间
  • len
    • 5:这个SDS保存了一个五字节长的字符串
  • buf:数组的前五个字节分别保存了Redis五个字符,而最后一个字节则保存了空字符\0

字符串类型分别使用REDIS_ENCODING_INTREDIS_ENCODING_RAW两种编码

  • REDIS_ENCODING_INT使用long类型来保存long类型值。
  • REDIS_ENCODING_RAW则使用sdshdr结构来保存sds(也即是char*)、long longdoublelong double类型值。

Redis将在数据库中创建了一个新的键值对,其中:

  • 键值对的键是一个字符串对象,对象的底层实现是一个保存着字符串"msg"的SDS
  • 键值对的值也是一个字符串对象,对象的底层实现是一个保存着字符串"hello world"的SDS

字符串使用场景

  • 用于保存单个字符串或者JSON数据
  • 可以用于保存图片(二进制数据)
  • 计数器(粉丝数量、点赞数)
  • 共享用户Session,集中管理

Hash类型

Hash类型是String的一个映射表,将一个对象类型储存在Hash类型比String类型占用更少的内存

就像python的dict,java的Map

  • hset KEY FIELD VALUE: 设定key,并设置字段
  • hmset KEY FIELD VALUE FIELD VALUE: 设定key,并设置多个字段
  • hget KEY FIELD: 获取值
  • hmget KEY FIELD FIELD: 获取多个值
  • hgetall KEY: 获取全部值
  • hdel KEY FIELD: 删除值
  • hsetnx KEY FIELD VALUE: 如果没有才设置值
  • hlen KEY: 字段数量
  • hkeys KEY: 获取哈希表中所有字段
  • hincrby KEY FIELD NUM: 自增一定数量
  • hincrbyfloat KEY FIELD NUM: 自增一定浮点数量
  • hexists KEY FIELD: 检测存在

Hash使用场景

  • 储存一个对象

可以简化key的name,节省内存

List类型

类似python的List,既可以作为栈,也可以作为队列

  • lpush KEY VALUE VALUE: 从头部插入
  • rpush KEY VALUE VALUE: 从尾部插入
  • lpushx KEY VALUE: 插入一个值到头部,List不存在则无效
  • rpushx KEY VALUE: 插入一个值到尾部,List不存在则无效
  • llen KEY: 获取列表长度
  • lindex key index: 通过索引获取值
  • lrange KEY START END: 获取范围的值
  • lpop KEY: 头部删除第一个,并返回值
  • rpop KEY: 尾部删除第一个,并返回值
  • blpop KEY KEY TIMEOUT: 堵塞删除,如果list没有元素会堵塞
  • lset KEY INDEX VALUE: 修改
  • linsert KEY before|after ITEM VALUE: 将值插入在ITEM的之前|之后
  • rpoplpush KEY KEY: 移除列表最后一个元素,并添加到另外一个列表里
  • brpoplpush KEY KEY TIMEOUT: 从列表弹出一个值插入到另外一个列表,并返回

可以使用负值索引

List使用场景

  • 对数据量大的集合的数据删减
    • 关注列表,热点新闻
  • 任务队列
    • rpush生产消息,lpop消费消息,sleep等待一会后重试
    • blpop,在没有消息的时候,它会阻塞住直到消息到来
  • 文章列表

Set类型

无序即可,类比python的Set

  • sadd KEY VALUE VALUE: 添加元素
  • scard KEY: 返回数量
  • smembers KEY: 返回集合中的所有元素
  • sismember KEY VALUE: 判断是否存在元素
  • srandmember KEY NUM: 随机返回num个元素
  • srem KEY VALUE VALUE: 移除元素
  • spop KEY NUM: 随机移除元素
  • SMOVE KEY KEY VALUE: 转移元素
  • sdiff KEY KEY:差集(左侧)
  • sinter KEY KEY: 交集
  • sinterstore DEST KEY KEY: 所有交集并储存在dest里
  • sunion KEY KEY: 并集
  • sunionstore KEY KEY: 所有并集并储存在dest里

Set使用场景

  • 对集合的操作
    • 兴趣圈子
    • 共同爱好
  • 唯一性:活跃用户列表
  • 抽奖

ZSet类型

有序Set,每个元素都会关联一个double类型的分数,通过这个分数进行排序。成员是唯一的,但是分数值可以重复

  • zadd KEY SCORE VALUE SCORE VALUE: 添加元素
  • zcard KEY: 元素数量
  • zount KEY MIN MAX: 获取指定分数区间的元素数量
  • zrank KEY VALUE: 返回元素索引
  • zrange KEY START END: 返回指定下标区间的元素(从低到高)
  • zrangebyscore KEY MIN MAX: 返回指定分数区间的元素(从低到高)
  • zrevrange KEY MIN MAX: 返回指定下标区间的元素(从高到低)
  • zrevrangebyscore KEY MAX MIN: 返回指定分数区间的元素(从高到低)
  • del KEY: 删除集合
  • zrem KEY VALUE VALUE: 删除元素
  • zremrangebyrank KEY START END: 删除指定下标区间元素
  • zremrangebyscore KEY MIN MAX: 删除指定分数区间元素
  • zincrby KEY NUM VALUE: 增加成员的分数,返回更改后的分数

ZSet使用场景

  • 排行榜
  • 权重队列

HyperLogLog

探索HyperLogLog算法

HyperLogLog算法经常在数据库中被用来统计某一字段的Distinct Value
基数就是指一个集合中不同值的数目,比如[a, b, c, d]的基数就是4,[a, b, c, d, a]的基数还是4,HyperLogLog通过记录一个hash值来区别元素,而不用储存元素本身

  • pfadd KEY VALUE VALUE: 添加元素进基数计算
  • pfcount KEY: 计算基数
  • pfmerge KEY KEY1 KEY2: 合并key

估计基数的算法

假设抛硬币的结果为1110100110,最长的反面序列为00,我们加上后一个元素001,这个序列出现概率为1/8,所以大概抛了8次

1
2
3
4
5
6
7
8
9
10
# 输入:一个集合
# 输出:集合的基数
# 算法:
max = 0
对于集合中的每个元素:
hashCode = hash(元素)
num = hashCode二进制表示中最前面连续的0的数量
if num > max:
max = num
最后的结果是2的(max + 1)次幂

此算法有0.81%的误差

HyperLogLog使用场景

  • 统计注册IP
  • 统计在线用户数量
  • 真实文章阅读数

发布订阅

Pub/Sub

  • psubscribe NACHANNELME: 订阅一个或多个符合给定模式的频道
  • subscribe CHANNEL: 订阅给定的一个或多个频道的信息
  • punsubscribe CHANNEL: 退订所有给定模式的频道
  • unsubscribe CHANNEL: 指退订给定的频道
  • publish CHANNEL MESSAGE: 将信息发送到指定的频道
  • pubsub: 查看订阅与发布系统状态

发布订阅使用场景

  • 生产一次消费多次:1:N消息队列
  • 只适合简单的消息队列,消费者下线的时候,生产的消息可能丢失

多数据库

默认连接0号数据库

  • select INDEX: 选择数据库
  • move KEY: 移动key
  • flushdb: 清空当前数据库
  • flushall: 清空所有数据库

多数据库可以对应不同业务功能

事务

Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  • 批量操作在发送 EXEC 命令前被放入队列缓存
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中

一个事务从开始到执行会经历以下三个阶段

  • 开始事务
  • 命令入队
  • 执行事务

以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务

  • multi: 开始一个事务
  • exec: 执行一个事务
  • unwatch: 取消对所有key的监控
  • discard: 取消十五
  • watch KEY: 监控一个或多个KEY,若事务之前之前key被改动,则事务将被打断
1
2
3
4
multi
...
...
exec

Redis事务确实不具备原子性的特征,事务中命令执行失败时,是不会回滚的。

  • 如果执行的某个命令爆出了错误,只有报错的命令不会执行,而其他命令都会执行,不会回滚
  • 如果队列中某个命令出现了报告错误,整个队列都会被取消

持久化

  • RDB做镜像全量持久化
    • RDB会耗费较长时间,不够实时
  • AOF做增量持久化

在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。

RDB

            graph LR
            内存中的数据对象--rdbSave-->磁盘中的RDB文件
磁盘中的RDB文件--rdbLoad-->内存中的数据对象
          

RDB是redis默认的持久化机制,在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中。创建一个子进程进行备份,子父进程共享数据段。

  • 优点
    • 适合大规模的数据恢复
    • 如果业务对数据完整性和一致性要求不高,RDB是很好的选择
  • 缺点
    • 数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机
    • 备份时占用内存,因为Redis在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍),最后再将临时文件替换之前的备份文件

AOF

弥补RDB的不足

            graph LR
            内存中的数据对象--flushAppendOnlyFile-->磁盘中的AOF文件
          

采用日志的形式来记录每个写操作,每次都会将新增的改变写入到磁盘里面,但是每次命令都会被保存。如果采取定时AOF,则可能丢失定时时间间隔的数据

  • 优点:数据的完整性和一致性更高
  • 缺点:因为AOF记录的内容多,文件会越来越大,数据恢复也会越来越慢

缓存与数据库一致性

  • 实时同步
    • 对强一致要求比较高的,需要实时同步
      • 即查询缓存查询不到则访问数据库,保存到缓存
      • 更新缓存时,先更新数据库,再设置缓存为过期
  • 异步队列
    • 对于并发较高,采用异步方式
    • 用定时任务方式进行同步
    • 异步队列用于流量的削峰

采取中间件进行

  • RabbitMQ:可靠性最高
  • rockermq: apache
  • kafka: 免费,性能高,顺序IO存储

缓存攻击

  • 缓存穿透: 不断查询不存在的数据,导致数据库压力过大
    • 用户验证后才查询
    • 设置缓存结果为空
    • 更新数据时,要更新缓存
  • 缓存雪崩:缓存大量失效时,数据库查询请求激增,且刚刚活过来又死去
    • 对数据库访问进行限流
    • 过期时间均匀分布
    • 储存数据,失效时间加上随机值
    • 数据预热
  • 热点Key: 对于某一个Key访问非常频繁,如果为失效会创建大量队列
    • 使用锁(locl,分布锁)
    • 不设置过期,设置下次更新时间,异步更新

Cluster集群

至少需要3Master+3Slave才能建立集群,采用无中心结构,网状连接

  • 所有redis阶段互相连接(PING-PONG机制),内部彼此使用二进制协议优化传输
  • 节点失败时通过集群中超过半数的节点检测失败才生效
  • 客户端与redis直接连接,不需要代理层
  • 集群预先分配16384个哈希槽,每个key会对于一个哈希槽,根据节点数量均等分配
  • 多个Redis间节点间共享数据的程序集
  • Redis集群无中心节点

Redis Cluster目前的一个欠缺之处:缺少结点的自动发现功能


  • Redis Sentinel(哨兵):着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
  • Redis Cluster(集群):着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

集群分片策略

集群将整个数据库分为16384个槽位slot,所有key-value数据都存储在这些slot中的某一个上。一个slot槽位可以存放多个数据,key的槽位计算公式为:slot_number = crc16(key) % 16384,其中crc16为16位的循环冗余校验和函数。

集群中的每个主节点都可以处理0个至16383个槽,当16384个槽都有某个节点在负责处理时,集群进入上线状态,并开始处理客户端发送的数据命令请求。

主节点只会处理自己负责槽位的命令请求,其它槽位的命令请求,该主节点会返回客户端一个转向错误

客户端重定向

由于Redis集群无中心节点,请求会随机发给任意主节点,主节点只会处理自己负责槽位的命令请求,其它槽位的命令请求,该主节点会返回客户端一个转向错误。客户端根据错误中包含的地址和端口重新向正确的负责的主节点发起命令请求。

主从模式

Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。

单台Redis内存不应该超过20GB

            graph LR
            Master-->Slave1
Master-->Slave2
Master-->Slave3
          

将Master只处理写请求,Slave只处理读请求

  • 高可用性

只需要配置文件即可实现主从复制

1
slaveof ip port
  • 主从模式下,当某一节点损坏时,因为其会将数据备份到其它Redis实例上,这样做在很大程度上可以恢复丢失的数据。
  • 主从模式下,可以保证负载均衡
  • 读写分离
  • 从节点写入数据不会进行同步

哨兵模式

Master如果down掉,把另外一个slave当作master(standby)

类似路由器的HSRP

  • 监控Redis是否良好运行
  • 如果发现某个redis节点出问题,能够通过另外一个进程
  • 能够选举出一个slave重写作为master
  • 能够为其他slave提供master地址(服务发现)

可以采用心跳机制来监视

集群容错

  • 判断master不可用: 投票机制
  • 判断集群不可用: Master down掉后,如果没有slave,则不可用

单线程

Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写。

  • 纯内存
  • 非阻塞IO
  • 数据结构简单
  • 避免线程切换和竞争消耗
    • 不用去考虑各种锁的问题
    • 不存在加锁释放锁操作
  • 一次只运行一条命令

keys指令

Redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

KV、DB读写模式

最经典的缓存+数据库读写的模式:Cache Aside Pattern

读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存。

为什么是删除缓存,而不是更新缓存?:可能频繁更新,但是不一定频繁被访问到

Redis分布式锁

  • 互斥性:在任意时刻,只有一个客户端(进程)能持有锁
  • 安全性:避免死锁情况,当一个客户端在持有锁期间内,由于意外崩溃而导致锁未能主动解锁,其持有的锁也能够被正确释放,并保证后续其它客户端也能加锁
  • 可用性:分布式锁需要有一定的高可用能力
  • 对称性:对同一个锁,加锁和解锁必须是同一个进程

分布式锁常见实现方式:

  • 通过数据库方式实现:采用乐观锁、悲观锁或者基于主键唯一约束实现
    • 基于mysql表唯一索引
    • 基于MongoDB findAndModify原子操作
  • 基于分布式缓存实现的锁服务
    • Redis
    • Redis的RedLock
  • 基于分布式一致性算法实现的锁服务
    • ZooKeeper
    • Chubby
    • Etcd

加锁

1
SET lock_name my_random_value NX PX 30000
  • lock_name:锁的名称,具有唯一性。
  • random_value:由客户端生成的一个随机字符串
    • 它要保证在足够长的一段时间内,且在所有客户端的所有获取锁的请求中都是唯一的,用于唯一标识锁的持有者。
  • NX:只有当 lock_name(key) 不存在的时候才能 SET 成功
    • 从而保证只有一个客户端能获得锁,而其它客户端在锁被释放之前都无法获得锁。
  • PX 30000:表示这个锁节点有一个 30 秒的自动过期时间
    • 防止持有锁的客户端故障后,无法主动释放锁而导致死锁
    • 要求锁的持有者必须在过期时间之内执行完相关操作并释放锁
    • 这个过期时间需要结合具体业务综合评估设置

解锁

1
del lock_name

实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好,但不一定高可用

Memcached区别

  • Redis支持复杂的数据结构
  • Redis原生支持集群模式
  • Memcached可以使用多核心

参考