Redis深度历险学习笔记-1

77 阅读9分钟
 #ubuntu install approach
 apt install redis

5 种基础数据结构 string list hash set zset(有序集合)

string

redis 的字符串是可修改的动态字符串, 类似于 ArrayList, 内部为当前字符串分配 capacity 一般高于实际长度 len, 字符串最大长度是 512MB, 字符串的扩容细节后面再看

常见用途是缓存信息 信息结构体使用 json 序列化成字符串放进 redis 缓存; 当然, 取信息时需要做一次反序列化

 $ 基础操作
 set key value
 get key 
 exists key # return (integer) 1
 del key # return (integer) 1
 ​
 $ 批量读写字符串
 mget key1 key2 key3 # 返回一个列表
 mset key1 v1 key2 v2 key3 v3 
 ​
 $ 过期和set命令扩展
 expire key expire_time # (such as 5 默认单位是秒)
 setex key expire_time value # 等价于 set + expire
 setnx key value # 如果key不存在就执行创建 创建返回 1 ; 未创建返回 0 (意为已存在同名的key)
 ​
 $ 计数
 如果value的值是一个整数, 可以对它进行自增操作, 自增是有范围的 signed long ; 溢出会报错 
 incr key
 incrby key 5

list

相当于 LInkedList; 是链表而不是数组 -> 因此插入和删除操作很快, 索引很慢

双向指针顺序, 支持前向后向遍历

底层存储是 quicklist(结合链表和 ziplist),多个 ziplist 使用双向指针串联

常用于异步队列使用

 $ 基础操作
 lpush/rpush key v1 v2 ...
 lpop/rpop key
 llen key
 $ 慢操作(lindex ltrim)
 lindex key index # -1表示倒数第一个元素
 lrange key 0 -1 # 获取所有元素
 ltrim key 1 0 # 清空整个列表

hash

相当于 HashMap;无序字典,可存储多个键值对,实现结构也与 HashMap 一样:数组加链表的二维结构;碰撞时将元素使用链表串联

redis 字典的值只能是字符串,使用渐进式 rehash 策略:rehash 的同时保留新旧两个 hash 结构,查询时同时查询两个 hash 结构,一点点地迁移,移除最后一个元素后,删除旧的 hash 结构

hash 结构也可以用来存储用户信息,与字符串需要一次性全部序列化整个对象不同,hash 可以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话,就只能一次性全部读取,这样就会浪费网络流量。

hash 也有缺点,hash 结构的存储消耗要高于单个字符串。到底该使用 hash 还是字符串,需要根据实际情况再三权衡。

 hset # 也可用于更新操作
 hget
 hgetall 
 hlen
 hmset # 批量 set
 hincrby # 和 incr 使用方法一样

set

相当于 HashSet; 内部键值对是无序的, 唯一的; 相当于一个特殊的字典, 字典中所有的 value 都是一个值 NULL

常用功能就是去重

zset

有序列表; 类似于 SortedSet&HashMap 地结合体; 内部实现是跳表

内部 value 唯一; 赋予每个 value 一个 score, 用来排序


容器型数据结构的通用规则

list set hash zset 是容器型数据结构

create if not exists

drop if no elements

过期时间

所有的数据结构都可以设置过期时间

分布式锁

原子操作:不会被线程调度机制打断的操作;一旦开始就会一直运行到结束,中间不会有线程切换

在 Redis 中的本质是占位

一般使用 setnx 来实现,完成后调用 del 释放

防止出现异常后锁不被释放,可以加上一个比较长的过期时间

这样的问题是 setnx 和 expire 是两条指令,从而引出了一些 set ex nx

不适用于较长时间的任务

将 value 设置为某个随机数,释放锁时先检验随机数是否一致,再决定是否执行删除操作(但是检验 value 和删除 key 也不是一个原子操作-> 必须使用 lua脚本 了, 因为 Lua 脚本可以保证多个指令的原子性执行

可重入性

可重入性指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次枷锁,那么这个锁就是可重入的

Redis 分布式锁如果要支持可重入,需要对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量存储当前持有锁的计数

使用可重入锁, 会加重客户端的复杂性, 在编写业务方法时注意在逻辑结构上进行调整完全可以不使用可重入锁

延时队列

Rabbitmq Kafka 作为消息队列中间件

对于只有一组消费者的消息队列, 使用 Redis 可以轻松搞定

异步消息队列

可以使用 list 来实现

空队列的处理

对空队列的 pop 会造成空轮询, 拉高 CPU 消耗, Redis 的 QPS 也会被拉高

通常使用 sleep 来解决, 让线程小睡一会

阻塞读

blpop/brpop b 代表 blocking

阻塞读在队列没有数据的时候, 会被阻塞, 进入休眠状态

空闲连接自动断开

服务器一般会主动断开连接, 减少闲置资源占用

锁冲突处理

  1. 直接抛出异常, 通知用户重试
  2. sleep 一会再重试
  3. 将请求转移至延时队列, 过会儿再试 适合异步消息处理

延时队列的实现

可以使用 zset 来实现

将消息序列化成一个字符串作为 zset 的 vlaue, 消息的到期处理时间作为 score, 然后 用多个线程轮询 zset 获取到期的任务进行处理

多个线程是为了保障可用性, 需要考虑并发争抢任务, 确保任务不会被多次执行

zrem 方法是返回值争抢任务的关键, 通过 zrem 方法的返回值来决定唯一的属主

bitmap 数据结构

本质内容其实就是字符串(byte 数组)

get set getbit setbit

redis 的位数组是自动扩展的, 如果某个偏移位置超出现有的内容范围, 就会自动将为数组进行零扩充

位数组的顺序和字符的位顺序是相反的

 bitcount 
 bitpos #用来查找指定范围内出现的第一个0或1
 bitfield # 可以一次操作多个位 
     bitfield get/set/incrby 一次最多处理64个连续的位,但是bitfield可以一次执行多个子指令

四种统计类型:

  • 二值状态统计
  • 聚合统计
  • 排序统计
  • 基数统计

HyperLogLog

提供不精确的去重计数

image-20240112144231749

高级数据结构; 用来做基数统计; 基数估计是在误差可接受的范围内, 快速计算基数

优点是: 在输入元素的数量或者体积非常大时, 计算基数所需的空间总是固定的且很小

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。

 pfadd
 pfcount
 pfmerge

具有去重功能; 不适合统计单个用户相关的数据

数据量足够大的时候, 误差在 1% 左右

实际存储

在计数比较小时, 它的存储空间采用稀疏矩阵存储, 空间占用很小

稀疏矩阵占用空间超过阈值后, 才会一次性转变为稠密矩阵, 占用 12KB

布隆过滤器

Bloom Filter 在起到去重作用的同时, 在空间上可以节省 90% 以上, 但也有一定的误判概率

image-20240112160552815

~的概念

当作一个不精确的 set 即可, 使用 contains 方法判断某个对象是否存在时, 可能会误判

image-20240112161049562

Redis 中的布隆过滤器

作为一个插件加载到 Redis Server 中

用法

 bf.add
 bf.exists
 bf.madd # 添加多个元素
 bf.mexists # 检查多个
 bf.reserve key error_rate capacity # 创建一个自定义的布隆过滤器 有三个参数
 bf.reserve a-filter 0.0001 10000000

期望错误率越低, 需要的空间就越大

实际元素数量超过初始化容量时, 误判率会上升

默认参数是 error_rate = 0.01 capacity = 100

因此在使用之前需要尽可能地精确估计元素数量

原理

空间占用估计

实际元素超出, 误判率如何变化

一个公式

f = (1-0.5^t)^k

极限近似, k 是 hash 函数的最佳数量

image-20240112164736260

简单限流

漏斗限流

GeoHash

比较通用的地理位置距离排序算法

内部结构实际上是一个 zset(skiplist),score 排序得到坐标附近的其他元素,通过还原 score 为坐标值得到元素的原始坐标

 geoadd
 zrem # 用来删除
 geodist # 用来计算两个元素之间的距离
 geopos # 获取集合中任意元素的经纬度坐标,可以一次获取多个
 geohash # 获取元素的经纬度编码字符串 (base32)
 georadiusbymember # 用来查询指定元素附近的其他元素 参数很复杂 
 georadius # 根据用户的定位来计算周边元素

在集群环境中单个 key 对应的数据量不宜超过 1MB, 否则会导致集群迁移出现卡顿现象, 影响线上服务的正常运行

建议 Geo 的数据使用单独的 Redis 实例部署, 不使用集群环境

数据量过大需要对 Geo 数据进行拆分(按某种属性特征)

scan 指令

redis 提供 keys 指令用来列出所有满足特定正则字符串规则的 key, 有两个缺点:

  • 没有 offset limit 参数, 会一次性输出所有满足条件的 key
  • keys 算法是遍历算法, 复杂度是 O(n) ; 且 Redis 是单线程程序, 顺序执行所有指令
  • 因此引入了 scan 指令

scan 指令的特点:

  • 复杂度也是 O(n) 但通过游标分步进行, 不会阻塞线程
  • 提供 limit 参数, 控制每次返回结果的最大条数
  • 同 keys, 也提供模式匹配功能
  • 服务器不需要为游标保存状态, 游标的唯一状态是 scan 返回给客户端的游标整数
  • 返回的结果可能会有重复, 需要客户端去重
  • 遍历过程中如果有数据修改, 改动后的数据能不能遍历到是不确定的
  • 单次返回的结果是空的并不意味着遍历结束, 而要看返回的游标值是否为 0
scan cursor(整数值) match 正则(key的正则模式) count(limit hint)

limit 限定的不是返回结果的数量, 而是限定服务器单次遍历的字典槽位数量(为等于)