目前在看钱文品老师的《Redis深度历险》,感觉蛮基础蛮有趣,就随手记录一些干货
文章目录
五种基础用法
kv键值对(string)
-
键值对读写
- “get {k}”, “set {k} {v}” 可以批量读写
- “mget {k1} {k2}”, “mset {k1} {v1} {k2} {v2}” 删除
- “del {k}” 可以查看和设置超时时间
- “ttl {k}”, “expire {k} {sec}” 检查某key是否存在
- “exist {k}” 可以防止覆盖
- “setnx {k} {v}” 用于记数的自增(可以指定大小)
- “incr {k}”, “incrby {k} {x}”
list
可以用作消息队列,但是机制过于简单功能过少,所以list不常用,因为涉及到索引遍历的操作都是n的时间复杂度
-
可以左右进出
- “lpush {k} {v1} {v2}”, “lpop {k}”, 和"rpush",“rpop” 求大小
- “llen {k}” 通过索引值获得元素
- “lindex {k} {idx}” 通过索引值的区间获得多个元素
- “lrange {k} {idx-begin} {idx-end}” -1表示结尾 通过索引值的区间修改list的内容
- “ltrim {k} {idx-begin} {idx-end}}” 负区间的话则清空
hash
优点是有独立的kv空间,如果对应到业务数据结构只是经常修改某字段,会省掉整体序列化很方便。
缺点是相较于kv会消耗额外空间。
ps:可以使用"structKey_propertyKey"方式拼接key把hash转为kv,酌情而定。
-
写
- “hset {k} {hk1} {hv1} {hk2} {hv2}” 读
- “hget {k} {hk}”, “hgetall {k}” 求大小
- “hlen {k}” 子key下的自增
- “hincrby {k} {hk} {x}”
set
set集合内的数据为无序去重的状态
-
写
- “sadd {k} {v1} {v2}” 读
- “smembers {k}” (全部返回), 弹出一个 “spop {k}” 球大小
- “scard {k}” 查是否存在
- “sismember {k} {v}”
zset
相对于set来说,zset内数据则为有序状态,需要给每个value一个score用于排序
-
写
- “zadd {k} {score} {v}” 按照排名读
- “zrange {k} {idx-begin} {idx-end}” 小到大顺序返回value,"zrevrange"倒叙 按照score区间读(可以把score一并取出)
- “zrangebyscore {k} {score-low} {score-high} (withscores)” 球大小
- “zcard {k}” 获取value的score(double类型)或排名(index)
- “zscore/zrank {k} {v}” 删除value
- “zrem {k} {v}”
常用场景
分布式锁
使用"setnx"来进行加锁,如果已有锁则会返回失败。但是在加锁之后产生异常会导致死锁,则需要再设置超时时间。2.8版本中加入了将两针合并的操作**“set {k} {v} ex {sec} nx”**。还可以在加锁解锁时value给签名来保证解锁解的是自己的锁。
消息队列
可以使用list结构的push和pop来实现消息队列。
防止一直pop空结果使cpu瞎忙,可以使用blpop/brpop阻塞弹出。
延时队列
使用zset结构的zrangebyscore和zrem实现。
存入任务时把score设置为开始处理的时间戳,然后处理线程每次都用当前时间戳做限制只取一条,取失败说明没有可处理休眠即可。取成功则用zrem删除,只有删除成功才能说明是自己抢到任务才可以处理。
因为两者不是原子操作并发高了时候会争抢严重,可以用lua封装。
有趣的工具
位图
对应“布尔型数组”,使用在用index来取bool值的场景,在kv键值对的基础上增加了对value字符串按位修改的操作。
读
: “getbit {k} {idx}”
-
写
- “setbit {k} {idx} 0/1”, “set {k} {v}” 可以整存(按照{v}的二进制存) 查找第一个0或1
- “bitpos {k} 0/1 ({start} {end})” 可以指定[start, end]范围,但是是按照字节索引的 统计1的个数
- “bitcount {k} ({start} {end})” 同上 bitfield 使位图支持区间上的操作,3.2更新
-
“bitfield {k} {cmd}” 其中,cmd有三种:
写
“set u/i{len} {idx} {v}” 使用无符号/有符号的v的二进制覆盖从idx开始长为len的部分
读
“get u/i{len} {idx}” 返回的是所取的二进制位区间按照u/i转换后的int
自增
“(overflow {op}) incrby u/i{len} {idx} {x}” x为自增的值
可以通过{op}选择当自增产生溢出时 保持最大值或者 失败不执行: sat/fail
HLL 基数估算
基数是一个数据集中,不重复的数据的个数
通常的场景都是使用一个set,然后把所有的数据都丢进去,最后求一个set的大小就结束了,费时费力的去重了的结果也没有用到。因此常用于数据统计。在这种情况下使用hyperloglog会很方便,但是注意这是估算会产生误差
-
原理
-
对于一个集合, 其中所有元素的二进制形式中,连续后缀零的个数的最大值与集合的基数是成正比的,例如1-8中最长的是8=>00001000,因此可以从最长的3个零估算出基数为2^3 = 8,1-8和1-15的集合求出来都是8,所以看的出来误差还是有的。
当然集合的元素不可能只是连续的1-8,如果出现一些特例比如8换成65536,就会导致最大值不是3而是更大。
因此要分成多份,每一份取各自的最大值,然后求得平均数来带入幂指函数。而平均数的算法使用调和平均数,这样会更加偏其中的小值。最终用平均数带入幂指函数算得了估计的基数。hyperloglog的讲解可以参考这里。 1
使用
-
“pfadd {k} {v1} {v2}” 给k集合添加元素
“pfcount {k}” 获取k集合基数
“pfmerge {k} {k1} {k2}” k为k1 k2的合并后的结果
实现
- 在redis内部,对于每一个pf计数都会分2^14个桶分组计数,然后每一个桶使用6位来记录后缀零的长度,最大能表示63位0,因此计算得内存占用12kb,所以在数量很大的时候使用这种方式会很简单。
布隆过滤器 4.0
布隆过滤器BloomFilter使用的场景一般是“垃圾过滤”。是在垃圾请求过多(不存在的请求过多的时候,多次的exist查询会拖慢数据库)或者不存在的请求代价过大。布隆过滤器就是可以过滤掉大部分肯定不存在的。而没被过滤掉的并不是一定存在,而是有一定的误差率会出现不存在的情况。
-
原理
-
布隆过滤器也是通过哈希实现的。hash在发生碰撞的时候需要定制方案去解决碰撞,而bf则不会去解决,误差就是因此而产生的。
bf使用一个大型的位数组用于散列,因为其结果只需要表示是否存在,则可以使用位来表示。然后bf使用多个哈希函数将结果散列在数组上,添加元素时只需将所有的散列结果位置1即可。查找的话只有当所有散列结果位都为1时才表示可能存在。
散列结果为0的可以知道是肯定没有出现过,是过滤掉的。而散列结果全为1的时候,可能是出现过也可能是全部发生碰撞产生的误差。
使用
-
“bf.add {k} {v}” 给过滤器k增加元素v
“bf.exists {k} {v}” 检查k中是否存在v元素
“bf.madd/mexists {k} {v1} {v2}” 也可以批量处理
“bf.reserve {k} {rate} {size}” 在add之前初始化,要指定key,错误率,基数大小
实现
- 因为bf的位数组只记录了哈希结果的位置,因此在在初始化的时候要确定大小,如果超过了规定的大小,错误率会上升但是不会飙升,也给了重建的时间。而因为bf内部完全没有存哈希结果,因此没有办法像hash一样重建。所以bf的重建只能 使用全部历史数据重新建立。
限流 4.0
对分布式集群的限流可以使用Redis-Cell,内部使用使用的是漏斗算法
-
使用
- “cl.throttle {k} {size} {op} {time} {ask}”
参数
:
key
漏斗总容量
操作次数
时间范围(用于计算速率“操作次数/时间范围”,例如“15 60”表示每60s可使用15个单位)
本次申请的大小(默认1)
返回
:
分配结果(1拒绝/0成功)
容量
剩余容量
多久后可重试(如果被拒绝)
多久后完全空
地理计算 3.2
geo指令,是普通的zset结构,元素的删除可以使用zrem。
geo内元素不宜太多,若需要一个数量级很大的geo,单独布置会避免redis卡顿
使用
:
“geoadd {k} {lon} {lat} {hk}” 在{k}下增加点{hk}, {lon}经度double[-180,180], {lat}纬度double[-90,90],东北为正。
“geodist {k} {hk1} {hk2} {type}” 用于计算{k}下两点之间距离,{type}为单位可选m/km/ml(英里)/ft(尺)
“geopose {k} {hk1} {hk2}” 取点的坐标,可取多个
“geohash {k} {hk}” 取点的哈希值
“georadiusbymember {k} {hk} {dis} {type} (withcoord/dist/hash) count {n} asc/desc”
取指定点附近的其他点,自己点也会返回
dis是附近的范围,type是单位,n为取的数量,可升序降序,可以一并取坐标/距离/哈希值
“georadius” 含义相同,将上面的{hk}某点改成{lon}{lat}坐标
遍历
用于管理全量的key的场景
使用
:
“keys {k}” 支持对key正则模式匹配,缺点是O(n)的时间而且还会卡住redis
“scan {iter} (match {k}) (count {n})” 使用{iter}游标遍历,返回新游标加数据list
第一次时用游标写0,后续则用返回的新游表,直到为0时遍历结束。支持模式匹配k和指定返回数量n
scan通过高位加法的方式遍历哈希表的slot,例如增序 000 -> 100 -> 010 -> 110
因为这种方式的遍历在rehash的前后顺序不变,不需要重新遍历。因为000rehash后变成0000和1000,新表中也是相邻的。
注意,在rehash的过程中可能出现重复的数据。
PS
redis的key的value不应该过大,zset这样的结构也是,否则会在rehash时候产生卡顿。要寻找这样的大key可以使用:redis-cli -h 127.0.0.1 -p 7001 --bigkeys -i 0.1
可选参数-i每隔100条scan休眠0.1s可以防止线上ops过高。
http://dqyuan.top/2018/08/22/hyperloglog.html ↩︎