Redis
基本操作
Redis 的各种数据类型的常规基本操作
键操作(key)
键操作
Redis 里面不管是哪种数据类型,他都是一个个的键名,键名是不允许重复的。
部分命令
-
del key
:在 key 存在时删除 key -
exists key
:检查给定 key 是否存在 -
expire key seconds
:给 key 设置过期时间,秒 -
expireat key timestamp
:给 key 设置过期时间,时间戳 -
pexpire key milliseconds
:给 kye 设置过期时间,毫秒 -
pexpireat key milliseconds-timestamp
:给 key 设置过期时间,时间戳,毫秒 -
persist key
:移除 key 的过期时间,key 永久保存 -
pttl key
:返回 key 的剩余时间,毫秒 -
ttl key
:返回 key 的剩余时间,秒 -
randomkey
:从当前数据库中随机返回一个 key -
rename key newkey
:修改 key 的名称 -
renamenx key newkey
:仅当 newkey 不存在时,将 key 改名为 newkey -
type key
:返回 key 所存储的值的类型
表格
命令 | 说明 |
---|---|
del key |
在 key 存在时删除 key |
exists key |
检查给定 key 是否存在 |
expire key seconds |
给 key 设置过期时间,秒 |
expireat key timestamp |
给 key 设置过期时间,时间戳 |
pexpire key milliseconds |
给 kye 设置过期时间,毫秒 |
pexpireat key milliseconds-timestamp |
给 key 设置过期时间,时间戳,毫秒 |
persist key |
移除 key 的过期时间,key 永久保存 |
pttl key |
返回 key 的剩余时间,毫秒 |
ttl key |
返回 key 的剩余时间,秒 |
randomkey |
从当前数据库中随机返回一个 key |
rename key newkey |
修改 key 的名称 |
renamenx key newkey |
仅当 newkey 不存在时,将 key 改名为 newkey |
type key |
返回 key 所存储的值的类型 |
附录
字符串(string)
字符串
最基本的数据类型,使用场景很广,大部分都是用的这个,缓存数据
部分命令
-
set key value
:设置指定 key 的值 -
setex key seconds value
:设置指定 key 的值,并设置 key 的过期时间,秒 -
psetex key milliseconds value
:设置指定 key 的值,并设置 key 的过期时间,毫秒 -
setnx key value
:只有在 key 不存在时设置 key 的值 -
mset key value [key value]
:同时设置一个或多个键值对 -
msetnx key value [key value ...]
:同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在 -
get key
:获取指定 key 的值 -
mget key1 [key2]
:获取多个 key 的值 -
incr key
:将指定 key 中存储的数字自增 1 -
incr key increment
:将指定 key 中存储的数字自增给定值(increment) -
incrbyfloat key increment
:将 key 所储存的值加上给定的浮点增量值(increment) -
decr key
:将指定 key 中存储的数字自减 1 -
decrby key decrement
:将指定 key 中存储的数字减去给定值(decrement) -
getrange key start end
:返回 key 中字符串截取的子字符 -
getset key value
:将 key 的值设置为 value,返回 key 的旧值 -
strlen key
:返回 key 所存储的字符串的长度
表格
命令 | 说明 |
---|---|
set key value |
设置指定 key 的值 |
setex key seconds value |
设置指定 key 的值,并设置 key 的过期时间,秒 |
psetex key milliseconds value |
设置指定 key 的值,并设置 key 的过期时间,毫秒 |
setnx key value |
只有在 key 不存在时设置 key 的值 |
mset key value [key value] |
同时设置一个或多个键值对 |
msetnx key value [key value ...] |
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在 |
get key |
获取指定 key 的值 |
mget key1 [key2] |
获取多个 key 的值 |
incr key |
将指定 key 中存储的数字自增 1 |
incr key increment |
将指定 key 中存储的数字自增给定值(increment) |
incrbyfloat key increment |
将 key 所储存的值加上给定的浮点增量值(increment) |
decr key |
将指定 key 中存储的数字自减 1 |
decrby key decrement |
将指定 key 中存储的数字减去给定值(decrement) |
getrange key start end |
返回 key 中字符串截取的子字符 |
getset key value |
将 key 的值设置为 value,返回 key 的旧值 |
strlen key |
返回 key 所存储的字符串的长度 |
附录
哈希(hash)
哈希
hash 主要用来干嘛
部分命令
-
hset key field value
:将哈希表 key 中的字段 field 的值设置为 value -
hsetnx key field value
:只有在字段 field 不存在时,设置哈希表字段的值 -
hget key field
:获取存储在哈希表中指定字段的值 -
hmget key field1 [field2]
:获取所有给定字段的值 -
hmset key field1 value1 [field2 value2]
:同时将多个 field-value (域-值)对设置到哈希表 key 中 -
hgetall key
:获取在哈希表中指定 key 的所有字段和值 -
hkeys key
:获取哈希表中的所有 key -
hvals key
:获取哈希表中所有的值 -
hexists key field
:查看哈希表 key 中,指定的字段是否存在 -
hdel key field1 [field2]
:删除一个或多个哈希表字段 -
hlen key
:获取哈希表中字段的数量 -
hincrby key field increment
:为哈希表 key 中的指定字段的整数值加上增量 increment -
hincrbyfloat key field increment
:为哈希表 key 中的指定字段的浮点数值加上增量 increment
表格
命令 | 说明 |
---|---|
hset key field value |
将哈希表 key 中的字段 field 的值设置为 value |
hsetnx key field value |
只有在字段 field 不存在时,设置哈希表字段的值 |
hget key field |
获取存储在哈希表中指定字段的值 |
hmget key field1 [field2] |
获取所有给定字段的值 |
hmset key field1 value1 [field2 value2] |
同时将多个 field-value (域-值)对设置到哈希表 key 中 |
hgetall key |
获取在哈希表中指定 key 的所有字段和值 |
hkeys key |
获取哈希表中的所有 key |
hvals key |
获取哈希表中所有的值 |
hexists key field |
查看哈希表 key 中,指定的字段是否存在 |
hdel key field1 [field2] |
删除一个或多个哈希表字段 |
hlen key |
获取哈希表中字段的数量 |
hincrby key field increment |
为哈希表 key 中的指定字段的整数值加上增量 increment |
hincrbyfloat key field increment |
为哈希表 key 中的指定字段的浮点数值加上增量 increment |
附录
列表(List)
列表
list 主要是用来干嘛的
列表内元素可重复
部分命令
-
lpush key value1 [value2]
:将一个或多个值插入到列表头部 -
lpushx key value
:将一个值插入到已存在
的列表头部,返回列表长度 -
rpush key value
:将一个或多个值插入到列表尾部,返回列表长度 -
rpushx key value
:将一个或多个值插入到已存在
列表尾部,返回列表长度 -
lset key index value
:通过索引设置列表元素的值,索引从 0 开始 -
llen key
:返回当前列表长度,无则返回 0 -
lrange key start end
:返回队列中一个区间的元素,(key 0 -1)表示返回所有元素 -
lindex key index
:通过索引获取列表中的元素 -
lpop key
:移出并获取列表的第一个元素 -
rpop key
:移出并获取列表的最后一个元素 -
blpop key1 timeout
:移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 -
brpop key1 timeout
:移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 -
rpoplpush
:从一个队列中 pop 出元素并 push 到另一个队列,也可以是同一个队列 -
lrem key count value
:删除队列中左起指定数量的字符,$data = Redis::lrem($list, 1, 'yuanyuan');// 删除队列中左起(右起 count < 0) 1个值为(count = 0 移除表中所有与 VALUE 相等的值) 'yuanyuan'的元素(若有)
-
ltrim key start end
:保留左边起若干元素,其余删除,$data = Redis::ltrim($list, 1, 6);// 保留左边起第 1 个至第 6 个元素
-
linsert key before|after pivot value
:在列表的元素前或者后插入元素,$data = Redis::linsert('list2', 'before', 'ab1', '123');//表示在元素 'ab1' 之前插入 '123'
表格
命令 | 说明 |
---|---|
lpush key value1 [value2] |
将一个或多个值插入到列表头部 |
lpushx key value |
将一个值插入到已存在 的列表头部,返回列表长度 |
rpush key value |
将一个或多个值插入到列表尾部,返回列表长度 |
rpushx key value |
将一个或多个值插入到已存在 列表尾部,返回列表长度 |
lset key index value |
通过索引设置列表元素的值,索引从 0 开始 |
llen key |
返回当前列表长度,无则返回 0 |
lrange key start end |
返回队列中一个区间的元素,(key 0 -1)表示返回所有元素 |
lindex key index |
通过索引获取列表中的元素 |
lpop key |
移出并获取列表的第一个元素 |
rpop key |
移出并获取列表的最后一个元素 |
blpop key1 timeout |
移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 |
brpop key1 timeout |
移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 |
rpoplpush |
从一个队列中 pop 出元素并 push 到另一个队列,也可以是同一个队列 |
lrem key count value |
删除队列中左起指定数量的字符,$data = Redis::lrem($list, 1, 'yuanyuan');// 删除队列中左起(右起 count < 0) 1个值为(count = 0 移除表中所有与 VALUE 相等的值) 'yuanyuan'的元素(若有) |
ltrim key start end |
保留左边起若干元素,其余删除,$data = Redis::ltrim($list, 1, 6);// 保留左边起第 1 个至第 6 个元素 |
linsert key (before、after) pivot value |
在列表的元素前或者后插入元素,$data = Redis::linsert('list2', 'before', 'ab1', '123');//表示在元素 'ab1' 之前插入 '123' |
部分代码展示
附录
有序集合(Sset)
有序集合
有序集合能干嘛?
成员唯一,不允许重复
通过一个分数来排序的,分数可以重复
部分命令
-
zadd key score1 member1 [score2 member2]
:向有序集合添加一个或多个成员,或者更新已存在成员的分数 -
zcard key
:获取有序集合的成员数 -
zcount key min max
:计算在有序集合中指定区间分数的成员数 -
zincrby key increment member
:有序集合中对指定成员的分数加上增量 increment -
zinterstore destination numkeys key [key ...]
:计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 -
zlexcount key min max
:在有序集合中计算指定字典区间内成员数量 -
ZRANGE key start stop [WITHSCORES]
:通过索引区间返回有序集合指定区间内的成员 -
ZRANGEBYLEX key min max [LIMIT offset count]
:通过字典区间返回有序集合的成员 -
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]
:通过分数返回有序集合指定区间内的成员 -
ZRANK key member
:返回有序集合中指定成员的索引 -
ZREM key member [member ...]
:移除有序集合中的一个或多个成员 -
ZREMRANGEBYLEX key min max
:移除有序集合中给定的字典区间的所有成员 -
ZREMRANGEBYRANK key start stop
:移除有序集合中给定的排名区间的所有成员 -
ZREMRANGEBYSCORE key min max
:移除有序集合中给定的分数区间的所有成员 -
ZREVRANGE key start stop [WITHSCORES]
:返回有序集中指定区间内的成员,通过索引,分数从高到低 -
ZREVRANGEBYSCORE key max min [WITHSCORES]
:返回有序集中指定分数区间内的成员,分数从高到低排序 -
ZREVRANK key member
:返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 -
ZSCORE key member
:返回有序集中,成员的分数值 -
ZUNIONSTORE destination numkeys key [key ...]
:计算给定的一个或多个有序集的并集,并存储在新的 key 中 -
ZSCAN key cursor [MATCH pattern] [COUNT count]
:迭代有序集合中的元素(包括元素成员和元素分值)
表格
命令 | 说明 |
---|---|
zadd key score1 member1 [score2 member2] |
向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
zcard key |
获取有序集合的成员数 |
zcount key min max |
计算在有序集合中指定区间分数的成员数 |
zincrby key increment member |
有序集合中对指定成员的分数加上增量 increment |
zinterstore destination numkeys key [key ...] |
计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 |
zlexcount key min max |
在有序集合中计算指定字典区间内成员数量 |
ZRANGE key start stop [WITHSCORES] |
通过索引区间返回有序集合指定区间内的成员 |
ZRANGEBYLEX key min max [LIMIT offset count] |
通过字典区间返回有序集合的成员 |
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT] |
通过分数返回有序集合指定区间内的成员 |
ZRANK key member |
返回有序集合中指定成员的索引 |
ZREM key member [member ...] |
移除有序集合中的一个或多个成员 |
ZREMRANGEBYLEX key min max |
移除有序集合中给定的字典区间的所有成员 |
ZREMRANGEBYRANK key start stop |
移除有序集合中给定的排名区间的所有成员 |
ZREMRANGEBYSCORE key min max |
移除有序集合中给定的分数区间的所有成员 |
ZREVRANGE key start stop [WITHSCORES] |
返回有序集中指定区间内的成员,通过索引,分数从高到低 |
ZREVRANGEBYSCORE key max min [WITHSCORES] |
返回有序集中指定分数区间内的成员,分数从高到低排序 |
ZREVRANK key member |
返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 |
ZSCORE key member |
返回有序集中,成员的分数值 |
ZUNIONSTORE destination numkeys key [key ...] |
计算给定的一个或多个有序集的并集,并存储在新的 key 中 |
ZSCAN key cursor [MATCH pattern] [COUNT count] |
迭代有序集合中的元素(包括元素成员和元素分值) |
附录
集合(Set)
集合
set 无序集合可以用来干嘛?
集合成员是唯一的,不能重复
部分命令
-
sadd key member1 [member2]
:向集合中添加一个或多个成员 -
srem key member1 [member2]
:移除集合中的一个或多个成员 -
scard key
:获取集合的成员个数 -
sismember key member
:判断 member 元素是否是集合 key 的成员 -
smembers key
:返回集合中的所有成员 -
spop key
:移除并返回集合中的一个随机元素 -
smove source destination member
:将 member 元素从 source 集合移动到 destination 集合 -
srandmember key [count]
:返回集合中一个或多个随机数 -
sdiff key1 [key2]
:返回集合 1 和集合 2 的差集 -
sinter key1 [key2]
:返回集合 1 和集合 2 的交集 -
sunion key1 [key2]
:返回集合 1 和集合 2 的并集 -
sdiffstore destination key1 [key2]
:返回给定集合的差集并存储在 destination 集合中 -
SINTERSTORE destination key1 [key2]
:返回给定集合的交集并存储在 destination 集合中 -
SUNIONSTORE destination key1 [key2]
:返回给定集合的并集并存储在 destination 集合中
表格
命令 | 说明 |
---|---|
sadd key member1 [member2] |
向集合中添加一个或多个成员 |
srem key member1 [member2] |
移除集合中的一个或多个成员 |
scard key |
获取集合的成员个数 |
sismember key member |
判断 member 元素是否是集合 key 的成员 |
smembers key |
返回集合中的所有成员 |
spop key |
移除并返回集合中的一个随机元素 |
smove source destination member |
将 member 元素从 source 集合移动到 destination 集合 |
srandmember key [count] |
返回集合中一个或多个随机数 |
sdiff key1 [key2] |
返回集合 1 和集合 2 的差集 |
sinter key1 [key2] |
返回集合 1 和集合 2 的交集 |
sunion key1 [key2] |
返回集合 1 和集合 2 的并集 |
sdiffstore destination key1 [key2] |
返回给定集合的差集并存储在 destination 集合中 |
SINTERSTORE destination key1 [key2] |
返回给定集合的交集并存储在 destination 集合中 |
SUNIONSTORE destination key1 [key2] |
返回给定集合的并集并存储在 destination 集合中 |
附录
主要使用场景
Redis 能干嘛
- 记录帖子的点赞数、评论数和点击数 (hash)。
- 记录用户的帖子 ID 列表 (排序),便于快速显示用户的帖子列表 (zset)。
- 记录帖子的标题、摘要、作者和封面信息,用于列表页展示 (hash)。
- 记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重计数 (zset)。
- 缓存近期热帖内容 (帖子内容空间占用比较大),减少数据库压力 (hash)。
- 记录帖子的相关文章 ID,根据内容推荐相关帖子 (list)。
- 如果帖子 ID 是整数自增的,可以使用 Redis 来分配帖子 ID(计数器)。
- 收藏集和帖子之间的关系 (zset)。
- 记录热榜帖子 ID 列表,总热榜和分类热榜 (zset)。
- 缓存用户行为历史,进行恶意行为过滤 (zset,hash)。
用在哪些地方
string
- 缓存数据
限流、计数器、分布式锁、session共享
限流、降级、熔断、隔离
hash
可以用来存用户信息,一个用户就只有一个键,但是用户有多个属性 用户主页访问量 组合查询等
list
基本用来做队列处理
-
lpush + lpop = Stack(栈)
-
lpush + rpop = Queue(队列)
-
lpush + ltrim = Capped Collection(有限集合)
-
lpush + brpop = Message Queue(消息队列)
set
赞,踩,标签,好友关系
Sset
有序集合有分数概念,所以可以用来做排行榜
等
雪崩、击穿、穿透
雪崩
简单讲:就是 Redis 挂了或者大量的键失效(击穿是一个热点 key 失效),请求都打到数据库了,所以数据库叫一下就死了
Redis服务器挂了,所有流量全部打到数据库,数据库肯定挂了,如果这个时候,重启数据库,那么数据库只能又被新流量给打死
缓存雪崩的事前事中事后的解决方案如下。
-
事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
-
事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
-
事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
限流,限制每秒只能有多少个请求通过,其余的全部降级,返回默认值,友情提示重试,或者空白值
限流可以保证数据库不会挂,数据库不挂,那么对用户来说,至少有一部分用户的请求是可以被处理的,系统不挂,对用户来说,就是多刷新几次就可能刷新出来一次
击穿
缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
穿透
key 对应的数据在数据源并不存在
,每次针对此 key 的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,缓存
或 数据库
都没有
,若黑客利用此漏洞进行攻击可能压垮数据库。
解决方案一:将击透的 key 也缓存起来,但是时间不能太长,缺点就是无法过滤动态的 key,每次进来都是不同的 kye 那其实作用就很有限了
解决方案二:使用布隆过滤器: 热点数据等场景(具体看使用场景)
布隆过滤器是什么?
布隆过滤器可以理解为一个不怎么精确的 set 结构,当你使用它的 contains 方法判断某个对象是否存在时,它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。
当布隆过滤器说某个值存在时
,这个值可能不存在
;当它说不存在时
,那就肯定不存在
Redis持久化
Redis开启持久化
Redis支持 RDB
和 AOF
两种方案来实现持久化,默认会开启 RDB
- RDB 方式的持久化几乎不损耗 Redis 本身的性能,在进行 RDB 持久化时,Redis 主进程唯一需要做的事情就是 fork 出一个子进程,所有持久化工作都由子进程完成
RDB方式
采用 RDB
持久方式,Redis 会定期保存数据快照至一个 rbd 文件中,并在启动时自动加载 rdb 文件,恢复之前保存的数据。
默认的配置为:
意思为: 900秒检查一次,如果发生了1条以上的数据变更,则快照保存 300秒检查一次,如果发生了10条以上的数据变更,则快照保存 60秒检查一次,如果发生了10000条以上的数据变更,则快照保存
注:yum安装默认配置文件在 /etc/redis.conf,如果是自己编译安装的,自己找去吧
AOF
采用 AOF
持久方式时,Redis 会把每一个写请求都记录在一个日志文件里。
在 Redis 重启时,会把 AOF 文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。缺点和优点这一看就很明确了吧!
AOF 默认是关闭的,如要开启,进行如下配置: appendonly yes
Redis 为此提供了三种 fsync (把文件在内存中的部分写回磁盘) 配置
- appendfsync no:不进行 fsync,将 flush 文件的时机交给 OS 决定,速度最快
- appendfsync always:每写入一条日志就进行一次 fsync 操作,数据安全性最高,但速度最慢
- appendfsync everysec:折中的做法,交由后台线程每秒 fsync 一次
这样后期文件会越来越大,恢复时间也越来越长,咋办勒。
AOF rewrite功能
可以重写AOF文件,只保留能够把数据恢复到最新状态的最小写操作集。
Redis 在每次AOF rewrite 时,会记录完成 rewrite 后的 AOF 日志大小,当 AOF 日志大小在该基础上增长了 100% 后,自动进行 AOF rewrite,同时如果增长的大小没有达到 64mb,则不会进行 rewrite。
内存管理与数据淘汰机制
内存管理与数据淘汰机制
默认情况下,在32位OS中,Redis最大使用3GB的内存,在64位OS中则没有限制。
所以最好给设置一个最大可使用内存: maxmemory 100mb
在内存占用达到了 maxmemory
后,再向Redis写入数据时,Redis会:
-
根据配置的数据淘汰策略尝试淘汰数据,释放空间
-
如果没有数据可以淘汰,或者没有配置数据淘汰策略,那么Redis会对所有写请求返回错误,但读请求仍然可以正常执行
在为Redis设置 maxmemory
时,需要注意一下几点:
-
如果采用了Redis的主从同步,主节点向从节点同步数据时,会占用掉一部分内存空间
-
如果maxmemory过于接近主机的可用内存,会导致数据同步时内存不足。
-
所以设置的maxmemory不要过于接近主机可用的内存,留出一部分预留用作主从同步。
内存管理
当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap)。交换会让 Redis 的性能急剧下降,对于访问量比较频繁的 Redis 来说,这样龟速的存取效率基本上等于不可用。 在生产环境中我们是不允许 Redis 出现交换行为的,为了限制最大使用内存,Redis 提供了配置参数 maxmemory 来限制内存超出期望大小。 当实际内存超出 maxmemory 时,Redis 提供了几种可选策略 (maxmemory-policy) 来让用户自己决定该如何腾出新的空间以继续提供读写服务。
-
noeviction
不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。 -
volatile-lru
尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。 -
volatile-ttl
跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。 -
volatile-random
跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。 -
allkeys-lru
区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。 -
allkeys-random
跟上面一样,不过淘汰的策略是随机的 key。 -
volatile-xxx
策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰。
数据淘汰机制
-
volatile-lru:使用LRU算法进行数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了有效期的key
-
allkeys-lru:使用LRU算法进行数据淘汰,所有的key都可以被淘汰
-
volatile-random:随机淘汰数据,只淘汰设定了有效期的key
-
allkeys-random:随机淘汰数据,所有的key都可以被淘汰
-
volatile-ttl:淘汰剩余有效期最短的key
配置: maxmemory-policy volatile-lru
默认注释且值为:noeviction,即不进行数据淘汰
Redis集群
主从模式
哨兵模式
Cluster模式
主从模式和哨兵模式暂不介绍,主要介绍 cluster 模式
前置准备工作
拉取镜像
docker pull redis:7
创建网络
docker network create -d bridge my-net
查看创建得网络列表 docker network ls
查看网络详细信息 docker network inspect my-net
准备 redis.conf 文件
一个容器准备一个配置文件,这里用脚本创建,直接运行如下命令即可
批量重置脚本
当发生错误的时候,批量停止、删除容器并清理相关文件,方便重来
停止容器,删除容器,删除容器产生的数据文件
采用 docker compose
运行 docker compose
进入容器配置集群
采用 Docker 方式
在 Windows 上采用docker方式,启动多个容器来模拟集群
脚本批量启动 Redis 容器
进入容器配置集群
连接 Redis 查看信息
测试数据
可以看到,每次 set 操作的时候,会将相应的键分配到对应的卡槽,然后自动转到相应的机器上,get 的时候,同理
Windows下安装多个 Redis 实例
去 GitHub
上下载 Windows 版的 Redis(3.2.100 的 zip 版),点击此处
创建多个 Redis 实例
根据你需要启动的实例个数,创建相应的 conf
和 log
文件,文件名可以随便取,不过为了更加直观,这里统一采用带端口的文件名形式取名
conf
文件内容,根据你的端口,将配置文件中的端口号都替换掉
启动多个 Redis 服务
安装 Redis 系统服务 redis-server.exe --service-install redis.6380.conf --service-name redis6380
查看 Redis 服务,可以在此配置手动启动或者自动启动等
连接 Redis
删除 Redis 服务
运行 regedit
打开注册表编辑器
找到:计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\
找到对应的 Redis 服务,右键删除即可
下载并安装Ruby
下载 Ruby
:http://dl.bintray.com/oneclick/rubyinstaller/rubyinstaller-2.2.4-x64.exe
其他版本:http://dl.bintray.com/oneclick/rubyinstaller/
安装:略
安装 gem 驱动
下载 Ruby
环境下 Redis
的驱动:确保 gem
命令可用
redis-3.2.2.gem
下载地址(https://rubygems.org/downloads/redis-3.2.2.gem)
安装,下图的 --local 后面跟下载好的 gem 文件路径,自行替换即可
搭建集群
下载 Redis 官方提供的创建 Redis集群
的 ruby 脚本文件 redis-trib.rb
下载地址:https://raw.githubusercontent.com/MSOpenTech/redis/3.0/src/redis-trib.rb
如果无法下载,本文最后有附录,如果打开后是一个页面,就将页面上的内容全部不保存到本地
搭建 redis-trib.rb create --replicas 0 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384
测试
在 127.0.0.1:6380
中添加一个 key,刷新其他实例,可以发现,该 key 已同步过来
使用一个实例的不同数据库模拟
简单分配逻辑
哈希存储
数据一致性问题
Redis脚本
Redis脚本采用 Lua 解释器来执行脚本
语法:EVAL script numkeys key [key ...] arg [arg ...]
Laravel 代码示例
Redis键过期事件的应用
过期策略
过期的 key 集合
redis
会将每个设置了过期时间的 key
放入到一个 独立的字典
中,以后会 定时遍历
这个字典来删除到期的 key。
除了定时遍历之外,它还会使用 惰性策略来删除
过期的 key,所谓惰性策略就是在客户端访问这个 key 的时候,redis 对 key的过期时间进行检查,如果过期了就立即删除。
定时删除
是集中处理,惰性删除
是零散处理。
定时扫描策略删除
Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
-
从过期字典中随机 20 个 key;
-
删除这 20 个 key 中已经过期的 key;
-
如果过期的 key 比率超过 1/4,那就重复步骤 1;
LRU 算法删除
淘汰上次使用时间最早的,且使用次数最少的key
当访问某个元素时,将这个元素移动到头部,所以尾部的都是不被重用的,可以进行淘汰,这个的弊端就是:一个数据长期不使用,突然被访问到以后就到了头部。
Redis 使用的是一种近似 LRU 算法,在现有数据结构的基础上使用随机采样法来淘汰元素
懒惰删除
实际功能代码演示
Redis GEO
laravel示例代码
布隆过滤器
布隆过滤器简介
布隆过滤器:一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组,也称 位数组
初始默认值都是 0
添加数据
当要向布隆过滤器中添加一个元素 key 时,我们通过多个 hash 函数,算出多个值,然后将这些值所在的方格置为 1
判断数据是否存在
同理,通过多个 hash 函数计算出多个值,判断对应值是否为1
-
如果有一个不为 1 那么可以确定该数据一定不存在于这个布隆过滤器中
-
如果全为 1 那么这个数据
不一定
存在于这个布隆过滤器中,因为 hash 计算出来的结果又重复的
优缺点
-
优点,占用内存少,插入和查询速度都贼快
-
缺点,随着数据的增加,误判率会增加;无法判断数据一定存在;无法删除数据
如何选择哈希个数和过滤器长度
很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。
另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。
k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率
如何选择适合业务的 k 和 m 值呢,这里直接贴一个公式:
注意
Redis 因其支持 setbit 和 getbit 操作,且纯内存性能高等特点,因此天然就可以作为布隆过滤器来使用。但是布隆过滤器的不当使用极易产生大 Value,增加 Redis 阻塞风险,因此生成环境中建议对体积庞大的布隆过滤器进行拆分。
拆分的形式方法多种多样,但是本质是不要将 Hash(Key) 之后的请求分散在多个节点的多个小 bitmap 上,而是应该拆分成多个小 bitmap 之后,对一个 Key 的所有哈希函数都落在这一个小 bitmap 上。
附录
- 参考链接:https://zhuanlan.zhihu.com/p/43263751
<?php
/**
* 布隆过滤器:说存在,不一定存在;说不存在,那一定不存在
*
* User: 舒孝元
* Date: 2020/8/19 14:43
* Mail: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
*/
/**
* 布隆过滤器简介
* 介绍下Bloom Filter的基本处理思路:
* 申请一批空间用于保存0 1信息,再根据一批哈希函数确定元素对应的位置,如果每个哈希函数对应位置的值为全部1,说明此元素存在。
* 相反,如果为0,则要把对应位置的值设置为1。由于不同的元素可能会有相同的哈希值,即同一个位置有可能保存了多个元素的信息,从而导致存在一定的误判率。
* 如果申请空间太小,随着元素的增多,1会越来越多,各个元素冲突的机会越来越来大,导致误判率会越来越大。
* 另外哈希函数的选择及个数上也要平衡好,多个哈希函数虽然可以提供判断的准确性,但是会降低程序的处理速度,而哈希函数的增加又要求有更多的空间来存储位置信息。
*
* Bloom-Filter的应用。
* Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
* 例如邮件服务器中的垃圾邮件过滤器。
* 在搜索引擎领域,Bloom-Filter最常用于网络蜘蛛(Spider)的URL过滤,网络蜘蛛通常有一个 URL列表,保存着将要下载和已经下载的网页的URL,
* 网络蜘蛛下载了一个网页,从网页中提取到新的URL后,需要判断该URL是否已经存在于列表中。此时,Bloom-Filter算法是最好的选择。
* 比如说,一个象 Yahoo,Hotmail 和 Gmai 那样的公众电子邮件(email)提供商,总是需要过滤来自发送垃圾邮件的人(spamer)的垃圾邮件。
* 一个办法就是记录下那些发垃圾邮件的 email 地址。由于那些发送者不停地在注册新的地址,全世界少说也有几十亿个发垃圾邮件的地址,将他们都存起来则需要大量的网络服务器。
* 布隆过滤器是由巴顿.布隆于一九七零年提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。我们通过上面的例子来说明起工作原理。
* 假定我们存储一亿个电子邮件地址,我们先建立一个十六亿二进制(比特),即两亿字节的向量,
* 然后将这十六亿个二进制位全部设置为零。对于每一个电子邮件地址 X,我们用八个不同的随机数产生器(F1,F2, ...,F8) 产生八个信息指纹(f1, f2, ..., f8)。
* 再用一个随机数产生器 G 把这八个信息指纹映射到 1 到十六亿中的八个自然数 g1, g2, ...,g8。现在我们把这八个位置的二进制位全部设置为一。
* 当我们对这一亿个 email 地址都进行这样的处理后。一个针对这些 email 地址的布隆过滤器就建成了。(见下图)
* 现在,让我们看看如何用布隆过滤器来检测一个可疑的电子邮件地址 Y 是否在黑名单中。
* 我们用相同的八个随机数产生器(F1, F2, ..., F8)对这个地址产生八个信息指纹 s1,s2,...,s8,
* 然后将这八个指纹对应到布隆过滤器的八个二进制位,分别是 t1,t2,...,t8。如果 Y 在黑名单中,显然,t1,t2,..,t8 对应的八个二进制一定是一。
* 这样在遇到任何在黑名单中的电子邮件地址,我们都能准确地发现。
* 布隆过滤器决不会漏掉任何一个在黑名单中的可疑地址。
* 但是,它有一条不足之处。也就是它有极小的可能将一个不在黑名单中的电子邮件地址判定为在黑名单中,
* 因为有可能某个好的邮件地址正巧对应八个都被设置成一的二进制位。
* 好在这种可能性很小。我们把它称为误识概率。在上面的例子中,误识概率在万分之一以下。
* 布隆过滤器的好处在于快速,省空间。但是有一定的误识别率。常见的补救办法是在建立一个小的白名单,存储那些可能别误判的邮件地址。
*/
namespace App\Http\Controllers\Redis;
use App\Common\BloomFilterHash;
use App\Common\Tools;
use App\Constants\RedisDemo\RedisDemo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
/**
* Redis::setBit 偏移量为 0-2^32-1,所以生成的哈希不能大于 2^32 -1
* Redis::setBit 参考链接 https://redis.io/commands/setbit
* 注意,一旦完成了第一次分配,后续对相同键的SETBIT调用将不会有分配开销。
*
* 该布隆过滤器总位数为2^32位, 判断条数为2^30条. hash函数最优为3个.(能够容忍最多的hash函数个数)
*
* 注意, 在存储的数据量到2^30条时候, 误判率会急剧增加, 因此需要定时判断过滤器中的位为1的的数量是否超过50%, 超过则需要清空.
*/
class RedisBloomFilterController
{
public string $key = RedisDemo::REDIS_BLOOM_FILTER;
/**
* 位图思想,需要用到的哈希算法,一般1~3个就行
*/
private array $hashFunction = ['FNVHash', 'SDBMHash'];
public function __destruct()
{
// 设置 Redis 的过期时间
Redis::EXPIRE($this->key, 24 * 3600);
}
/**
* Description: 简单测试-位图思想
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/11 11:26
*
* @param Request $request
*
* @return array
*/
public function bloomIndex(Request $request): array
{
$key = $this->key;
// 偏移量
$offset = (int)$request->input('offset', 1);
/**
* Redis 最大只支持 0xFFFFFFFF == 4294967295 ≈ 512MB
* 注意:本地上测试的时候,会发现,经常 flash ,原因有以下几点
* 一:Redis配置文件里面设置了最大内存
* 二:本机没那么多空余内存分配
* 三:偏移量太大
*/
$old_status = Redis::setbit($key, $offset, 1);
$new_status = Redis::getbit($key, $offset);
$data = [
'redis_key' => $key,
'offset' => $offset,
'old_status' => $old_status,
'new_status' => $new_status,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 获取字符串经过哈希算法后的值
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/10 16:12
*
* @param Request $request
*
* @return array
*/
public function getHash(Request $request): array
{
$string = $request->input('string', 'shuxiaoyuan');
// 获取该类所有方法
$rc = new \ReflectionClass(BloomFilterHash::class);
// 默认获取所有,这里只拿静态方法
// 只获取 public 和 static 方法(\ReflectionMethod::IS_STATIC | \ReflectionMethod::IS_PUBLIC)
$reflection_methods = $rc->getMethods(\ReflectionMethod::IS_STATIC);
// dd($reflection_methods); // 返回包含每个方法 ReflectionMethod 对象的数组。
$data['string'] = $string;
foreach ($reflection_methods as $method) {
$name = $method->name;
$data['hash_value'][$method->name] = BloomFilterHash::$name($string);
}
return Tools::outSuccessInfo($data);
}
/**
* Description: 添加一个元素到布隆过滤器
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/10 18:04
*
* @param Request $request
*
* @return array
*/
public function bloomAddOne(Request $request): array
{
$key = $this->key;
$string = $request->input('string', 'shuxiaoyuan');
/**
* Redis 官方提供的布隆过滤器到了 Redis 4.0 提供了插件功能之后才正式登场。
* 布隆过滤器作为一个插件加载到 Redis Server 中,给 Redis 提供了强大的布隆去重功能。
*
* docker pull redislabs/rebloom
* bf.add 只能一次添加一个元素
* bf.madd 可以添加多个 bf.madd test user1 user2 user3 ……
*/
$script = <<<SCRIPT
return redis.call('bf.add', KEYS[1], ARGV[1])
SCRIPT;
$value = Redis::eval($script, 1, $key, $string);
// 采用位图思想
// $value = $this->addOne($key, $string);
$data = [
'redis_key' => $key,
'string' => $string,
'value' => $value,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 判断一个元素是否存在
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/19 16:30
*
* @param Request $request
*
* @return array
*/
public function bloomGetOne(Request $request): array
{
$key = $this->key;
$string = $request->input('string', 'shuxiaoyuan');
/**
* bf.exists 一次只能查询一个
* bf.mexists 一次可以查询多个 bf.mexists test user1 user2 user3 ……
*/
$script = <<<SCRIPT
return redis.call('bf.exists', KEYS[1], ARGV[1])
SCRIPT;
$value = Redis::eval($script, 1, $key, $string);
// 采用位图思想
// $value = $this->exists($key, $string);
$data = [
'redis_key' => $key,
'string' => $string,
'status' => $value,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 测试误判率1:查找存在的数据
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/20 11:07
*
*/
public function test1(Request $request): array
{
set_time_limit(0);
ini_set('memory_limit', '-1M');
$key = $this->key . 'tmp';
$number = (int)$request->input('number', 1000);
$insert = 0;
$exists = 0;
for ($i = 0; $i < $number; $i++) {
$script = <<<SCRIPT
return redis.call('bf.add', KEYS[1], ARGV[1])
SCRIPT;
$insert += Redis::eval($script, 1, $key, 'user' . $i);
$script = <<<SCRIPT
return redis.call('bf.exists', KEYS[1], ARGV[1])
SCRIPT;
// 经过测试,发现百分百无误判
$exists += Redis::eval($script, 1, $key, 'user' . $i);
}
Redis::del($key);
$data = [
'insert' => $insert,
'exists' => $exists,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 测试误判率2:查找不存在的数据
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/20 11:29
*
* @param Request $request
*
* @return array
*/
public function test2(Request $request): array
{
set_time_limit(0);
ini_set('memory_limit', '-1M');
$key = $this->key . 'tmp';
$number = (int)$request->input('number', 1000);
$insert = 0;
$exists = [];
for ($i = 0; $i < $number; $i++) {
$script = <<<SCRIPT
return redis.call('bf.add', KEYS[1], ARGV[1])
SCRIPT;
$insert += Redis::eval($script, 1, $key, 'user' . $i);
$script = <<<SCRIPT
return redis.call('bf.exists', KEYS[1], ARGV[1])
SCRIPT;
// 每次塞入时,肯定是没有 i+1 这个的
$tmp = Redis::eval($script, 1, $key, 'user' . ($i + 1));
if ($tmp) {
// 当数量为 1000 时,误判为:306,310,442,611,741,902
$exists[] = $i;
}
}
Redis::del($key);
$data = [
'insert' => $insert,
'exists' => $exists,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 测量误判率
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/20 11:34
*
* 测量方法:
* 我们先随机出一堆字符串,然后切分为 2 组,
* 将其中一组塞入布隆过滤器,然后再判断另外一组的字符串存在与否,
* 取误判的个数和字符串总量一半的百分比作为误判率。
*/
public function test3(Request $request): array
{
set_time_limit(0);
ini_set('memory_limit', '-1M');
$key = $this->key . 'tmp';
$number = (int)$request->input('number', 1000);
if ($number % 2) {
$number += 1;
}
$half = (int)bcdiv($number, 2);
for ($i = 0; $i < $half; $i++) {
$script = <<<SCRIPT
return redis.call('bf.add', KEYS[1], ARGV[1])
SCRIPT;
Redis::eval($script, 1, $key, 'user' . $i);
}
$count = 0;
for ($i = $half + 1; $i < $number; $i++) {
$script = <<<SCRIPT
return redis.call('bf.exists', KEYS[1], ARGV[1])
SCRIPT;
if (Redis::eval($script, 1, $key, 'user' . $i)) {
$count += 1;
}
}
Redis::del($key);
$data = [
'insert' => $number,
'count' => $count,
'proportion' => bcdiv($count, $half, 8)
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 添加一个元素到布隆过滤器,采用位图实现
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/10 17:27
*
* 这里采用的思想是用的位图实现,将 string 按照一定的 hash 函数计算后,将相应位置1
*
* @param string $string
* @param string $key
*
* @return mixed
*/
private function addOne(string $key, string $string): mixed
{
// 位图最大为 2^32 = 4294967296
$bit = [];
foreach ($this->hashFunction as $function) {
$bit[] = (int)BloomFilterHash::$function($string);
}
$redis_response = Redis::pipeline(function ($pipeline) use ($string, $key) {
foreach ($this->hashFunction as $function) {
$hash = (int)BloomFilterHash::$function($string);
$pipeline->setbit($key, $hash, 1);
}
});
return [
'bit' => $bit,
'redis_response' => $redis_response,
];
}
/**
* Description: 判断元素是否存在,采用位图实现
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/10 17:27
*
* 采用的思想是位图的思想,将 string 按照相同的 hash 函数计算后,取出相应的所有位,依次判断每一位是否为1
*
* @param $string
* @param $key
*
* @return bool
*/
private function exists(string $key, string $string): bool
{
// 采用管道
$dataResponse = Redis::pipeline(function ($pipeline) use ($string, $key) {
$len = strlen($string);
foreach ($this->hashFunction as $function) {
$hash = BloomFilterHash::$function($string, $len);
$pipeline->getBit($key, $hash);
}
});
foreach ($dataResponse as $bit) {
if ($bit == 0) {
return false;
}
}
return true;
}
}
位图应用
<?php
/**
*......................位图......................
* _oo0oo_
* o8888888o
* 88" . "88
* (| -_- |)
* 0\ = /0
* ___/`---'\___
* .' \\| |// '.
* / \\||| : |||// \
* / _||||| -卍-|||||- \
* | | \\\ - /// | |
* | \_| ''\---/'' |_/ |
* \ .-\__ '-' ___/-. /
* ___'. .' /--.--\ `. .'___
* ."" '< `.___\_<|>_/___.' >' "".
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
* \ \ `_. \_ __\ /__ _/ .-` / /
* =====`-.____`.___ \_____/___.-`___.-'=====
* `=---='
*
*..................佛祖开光 ,永无BUG...................
*
* Description: 请简单描述
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/2/28 15:52
*/
namespace App\Http\Controllers\Redis;
use App\Common\Tools;
use App\Constants\ErrorCode;
use App\Constants\RedisDemo\RedisDemo;
use App\Facades\ToolsFacade;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Validator;
/**
* Description: Redis 位图
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2021/12/29 17:38
*
* 基础知识:
* 1TB = 1024GB
* 1GB = 1024MB
* 1MB = 1024KB
* 1KB = 1024B (byte)
* 1B = 8b (bit位)
*
* 适用场景:
* 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等。
* 如果连续签到中断,则重置计数,每月初重置计数。
* 当月签到满3天领取奖励1,满5天领取奖励2,满7天领取奖励3……等等。
* 显示用户某个月的签到次数和首次签到时间。
* 在日历控件上展示用户每月签到情况,可以切换年月显示……等等。
*
* 最大位只能设置为 2^32 = (这会将位图限制为 512MB)
*
* Class RedisBitmapController
*
* @package App\Http\Controllers\Redis
*/
class RedisBitmapController
{
public string $key = RedisDemo::REDIS_BITMAP;
public string $tmp_key = RedisDemo::REDIS_BITMAP . ':tmp';
/**
* Description: 设置相应位为0或者1
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/14 18:00
*
* @param Request $request
*
* @return array
*/
public function setBit(Request $request): array
{
$key = $request->input('redis_key');
if (!$key) {
$key = $this->tmp_key;
}
$bit = (int)$request->input('bit', 66);
// 返回值:0或1,存储在偏移量的原始位值
Redis::setbit($key, $bit, 1);
$status = Redis::getbit($key, $bit);
$data = [
'bit' => $bit,
'redis_key' => $key,
'status' => $status,
'bitcount' => Redis::bitcount($key), // 一共有多少位设置成 1
];
Redis::expire($key, 8 * 2400);
return Tools::outSuccessInfo($data);
}
/**
* Description: 获取相应位是0还是1
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/14 18:07
*
* @param Request $request
*
* @return array
*/
public function getBit(Request $request): array
{
$key = $request->input('redis_key');
if (!$key) {
$key = $this->tmp_key;
}
$bit = (int)$request->input('bit', 66);
$status = Redis::getbit($key, $bit);
$data = [
'bit' => $bit,
'redis_key' => $key,
'status' => $status,
'bitcount' => Redis::bitcount($key), // 一共有多少位设置成 1
// 'value' => Redis::get($key), // 编码需要转换
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 获取指定范围内1的个数
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/14 18:12
*
* @param Request $request
*
* @return array
*/
public function getCountByStartToEnd(Request $request): array
{
$key = $request->input('redis_key');
if (!$key) {
$key = $this->tmp_key;
}
$start_bytes = $request->input('start_bytes', 0);
$end_bytes = $request->input('end_bytes', -1);
// 这里的开始和结束指 byte,从 0 开始算,0到2 查找的是 0到(2+1)*8=24位
// 0-0 就是 1byte 也指 0-7位
$bitcount = Redis::bitcount($key, $start_bytes, $end_bytes);
$data = [
'redis_key' => $key,
'start_bytes' => $start_bytes,
'end_bytes' => $end_bytes,
'bitcount' => $bitcount,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 获取指定范围内出现的第一个 0 或 1
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/17 14:10
*
* @param Request $request
* @param ErrorCode $errorCode
*
* @return array|void
*/
public function getBitPosByStartToEnd(Request $request, ErrorCode $errorCode)
{
$validator = Validator::make($request->all(), [
'start_bytes' => 'required|integer|min:0',
'end_bytes' => 'required|integer',
'status' => 'required|in:0,1',
], $message = [
'start_bytes.required' => '开始位未设置',
'start_bytes.integer' => '开始位必须为整数',
'start_bytes.min' => '开始位最小为1',
'end_bytes.required' => '结束位未设置',
'end_bytes.integer' => '结束位必须为整数',
'status.required' => '状态未设置',
'status.in' => '状态只能为0或1',
]);
if ($validator->fails()) {
return $errorCode->getValidatorErrorMessage($validator);
}
$key = $request->input('redis_key');
if (!$key) {
$key = $this->tmp_key;
}
$start_bytes = $request->input('start_bytes', 0);
$end_bytes = $request->input('end_bytes', -1);
$status = $request->input('status', 1);
// 这里的开始和结束指 byte,从 0 开始算
$bitpos = Redis::bitpos($key, $status, $start_bytes, $end_bytes);
$data = [
'redis_key' => $key,
'start_bytes' => $start_bytes,
'end_bytes' => $end_bytes,
'bitcount' => Redis::bitcount($key),
'bitpos' => $bitpos,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 设置位,简单字符串演示
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/7 16:07
*
* @param Request $request
*
* @return array
*/
public function setBitExample(Request $request): array
{
$key = $this->key;
$string = $request->input('string', 'shuxiao');
$ascii = [];
$bin = [];
$offset = [];
for ($i = 0; $i < strlen($string); $i++) {
// $o = $string[$i] . $o; // 反转字符串
// 将字符串转 ASCII 码: "s":115,"h":104,"u":117,"x":120,"i":105,"a":97,"o":111
$ascii[$string[$i]] = ord($string[$i]);
// 转成二进制值: "s":"1110011","h":"1101000","u":"1110101","x":"1111000","i":"1101001","a":"1100001","o":"1101111"
$bin[] = decbin(ord($string[$i]));
}
/**
* 字符 s=>1110011 和字符 h=>1101000 表示如下
* 高位 字符s 低位 高位 字符h 低位
* |0|1|1|1|0|0|1|1| |0|1|1 |0 |1 |0 |0 |0 | 用8位字节表示
* |7|6|5|4|3|2|1|0| |7|6|5 |4 |3 |2 |1 |0 | 8个字节中,左边为高位,右边为低位
* |0|1|2|3|4|5|6|7|……|8|9|10|11|12|13|14|15| 这个是位顺序
*
* 注意: 位数组的顺序和字符的位顺序是相反的
* 所以 s 需要设置的位是: 1、2、3、6、7
* Redis::setbit($key, 1, 1);
* Redis::setbit($key, 2, 1);
* Redis::setbit($key, 3, 1);
* Redis::setbit($key, 6, 1);
* Redis::setbit($key, 7, 1);
* // 应该输出一个 s
* dd(Redis::get($key));
*
* h 需要设置的位是: 9、10、12
* Redis::setbit($key, 9, 1);
* Redis::setbit($key, 10, 1);
* Redis::setbit($key, 12, 1);
* // 应该输出一个 sh
* dd(Redis::get($key));
*
*/
// 设置偏移量:主要是计算偏移量在多少位
foreach ($bin as $k => $v) {
for ($i = 0; $i < strlen($v); $i++) {
if ($v[$i]) {
// 计算偏移量
$bit = (int)bcmul($k, 8) + ($i + 1);
$offset[] = $bit;
Redis::setbit($key, $bit, 1);
}
}
}
$data = [
'输入的字符串' => $string,
'取出键值' => Redis::get($key),
'ascii码' => $ascii,
'二进制值' => $bin,
'需要设置的偏移量' => $offset,
];
Redis::del($key);
return Tools::outSuccessInfo($data);
}
/**
* Description: 用户签到
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/10 13:21
*
* @param Request $request
* @param ErrorCode $errorCode
*
* @return array
*/
public function setUserTask(Request $request, ErrorCode $errorCode): array
{
$validator = Validator::make($request->all(), [
'member_id' => 'required|integer|min:1',
'date' => 'required|date_format:Y-m-d',
], $message = [
'member_id.required' => '用户ID未设置',
'member_id.integer' => '用户ID必须为整数',
'member_id.min' => '用户ID最小为1',
'date.required' => '签到日期未设置',
'date.date_format' => '签到日期格式不正确',
]);
if ($validator->fails()) {
return $errorCode->getValidatorErrorMessage($validator);
}
$member_id = $request->input('member_id');
$date = $request->input('date');
$year = date('Y', strtotime($date));
// 将每一个用户的签到按照年份进行划分,Redis键按照会员和年来划分
$key = $this->key . ':' . $year . ':' . $member_id;
// 测试一年的每一天都签到
// $days = ToolsFacade::getEverydayDate($year . '0101', $year . '1231', 'nd');
/**
* 将该年中的月日作为位来设置,这样每一个键最大的存储空间为 1231(bit) / 8 = 154(Bytes)
* 因为月日不连续,并且第一天是101,所以这样会浪费一部分空间,可以进一步缩减到 365/366 天
*/
// // 缩短到 366 天方案
// // 今年的开始天到现在的天数
// $formatted_dt1 = Carbon::parse(Carbon::make($date)->firstOfYear()->toDateString());
// $formatted_dt2 = Carbon::parse($date);
// $date_diff_days = $formatted_dt1->diffInDays($formatted_dt2); // 这个就当做相应位
// dd($date_diff_days);
$day = date('md', strtotime($date));
// 返回值:0或1,存储在偏移量的原始位值
$status = Redis::setbit($key, (int)$day, 1);
$bitcount = Redis::bitcount($key);
// 入库持久化过程略
$data = [
'redis_key' => $key,
'status' => $status,
'count_number' => $bitcount,
];
Redis::expire($key, 8 * 3600);
return Tools::outSuccessInfo($data);
}
/**
* Description: 统计用户某段时间内的签到次数
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* Website: https://www.shuxiaoyuan.com
* DateTime: 2023/4/14 14:51
*
* @param Request $request
*
* @return array
*/
public function getUserTaskByStartToEnd(Request $request): array
{
$member_id = $request->input('member_id');
// 开始时间和结束时间-这里假设不考虑跨年的情况
$start_date = $request->input('start_date', '2023-01-01');
$end_date = $request->input('end_date', '2023-01-31');
$year = date('Y', strtotime($start_date));
$key = $this->key . ':' . $year . ':' . $member_id;
/**
* 方案一:根据开始时间和结束时间循环去取相应位,然后统计
*/
$method1 = Redis::pipeline(function ($pipeline) use ($key, $start_date, $end_date) {
$start_bytes = (int)date('md', strtotime($start_date));
$end_bytes = (int)date('md', strtotime($end_date));
for ($i = $start_bytes; $i < $end_bytes; $i++) {
$pipeline->getbit($key, $i);
}
});
// 后续研究下效率问题
$count1 = array_sum($method1);
// 如果是采用的缩短到 366 天方案
// $year_first_day = Carbon::parse(Carbon::make($start_date)->firstOfYear()->toDateString()); // 今年的第一天
// $start_bytes = $year_first_day->diffInDays($start_date);
// $end_bytes = $year_first_day->diffInDays($end_date);
// dd($start_bytes, $end_bytes);
/**
* 方案二:使用 bitcount 命令,将包含开始时间和结束时间内的统计出来,然后过滤掉不合法数据
* 采用默认即: 12B - 17B
*/
$start_bytes = floor(bcdiv((int)date('md', strtotime($start_date)), 8, 2));
$end_bytes = ceil(bcdiv((int)date('md', strtotime($end_date)), 8, 2));
// 这个范围内不仅包含了指定的日期,还包含了未指定的日期,所以要处理
$method2 = Redis::bitcount($key, $start_bytes, $end_bytes);
$tmp_start_bytes = bcmul($start_bytes, 8); // 96b
$tmp_end_bytes = bcmul($end_bytes, 8); // 136b
// 需要剔除的位
$exclude = Redis::pipeline(function ($pipeline) use ($key, $start_date, $end_date, $tmp_start_bytes, $tmp_end_bytes) {
$start_bytes = (int)date('md', strtotime($start_date)); // 101b
$end_bytes = (int)date('md', strtotime($end_date)); // 131b
// 96b - 101b
for ($i = $tmp_start_bytes; $i < $start_bytes; $i++) {
$pipeline->getbit($key, $i);
}
// 131b - 136b
for ($i = $end_bytes; $i < $tmp_end_bytes; $i++) {
$pipeline->getbit($key, $i);
}
});
// 因为只有0和1两个值,所以可以直接求和
$exclude_count = array_sum($exclude);
$count2 = $method2 - $exclude_count;
$data = [
'redis_key' => $key,
'start_date' => $start_date,
'end_date' => $end_date,
'count1' => $count1,
'method2' => $method2,
'count2' => $count2,
];
return Tools::outSuccessInfo($data);
}
/**
* Description: 查看用户某一天是否签到
* Author: Shuxiaoyuan
* Email: sxy@shuxiaoyuan.com
* DateTime: 2022/1/7 14:52
*
* @param Request $request
*
* @return array
*/
public function getUserIsTaskByDay(Request $request): array
{
$member_id = $request->input('member_id');
$date = $request->input('date');
$year = date('Y', strtotime($date));
// 将每一个用户的签到按照年份进行划分,Redis键按照会员和年来划分
$key = $this->key . ':' . $year . ':' . $member_id;
$day = (int)date('md', strtotime($date));
// 查找某一位上面的值
$status = Redis::getbit($key, $day);
$data = [
'redis_key' => $key,
'day' => $day,
'status' => $status,
];
return Tools::outSuccessInfo($data);
}
}
Redis HyperLogLog
laravel示例代码
查看单个key占用内存大小
memory usage key_name
单位为 bytes