REDIS 各种数据结构有什么作用?都能干什么?

string

第一、最常用的就是缓存对象,这没什么好说的。

第二、可以用 setnx 去实现分布式锁。

第三、可以实现共享 session。用户登录的时候生成一个 token,以 token 为 key,以用户信息为 value,给一个过期时间存到 REDIS 里面。用户一段时间没访问,系统就过期失效了,需要重新登录了。只要用户一访问,就通过拦截器给 token 续期,这个就是共享 session。

复制代码
##思路如下:
##用户登录后生成 token
String token = UUID.randomUUID().toString();
##以 token 为 key,以用户信息为 value,给一个过期时间,存到 redis 中
setex <token> <过期时间> < 用户信息 json>
##获取用户信息
get <token>
##判断用户是否登录 / 校验 token 有效性
exists <token>
##拦截器中 token 续期
expire <token> <新过期时间>

第四、string 原子性的自增操作。可以去做计数器,去记录某个网页的访问量或者记录视频的播放次数。以网页路径或者视频 id 为 key,以次数为 value,访问一次就自增一次,实现记录器的效果。

复制代码
##初始化计数器,如果 key 不存在,初始化为 0,假设视频 id 为 123
set video:play:123 0
##每次访问 / 播放时,执行原子自增
incr video:play:123 # 返回值就是自增后的值,如 1,2,3
##获取当前计数值
get video:play:123

第五、可以做计数器限流。比如针对 IP 或者接口做限流,以 IP 或者接口路径为 key,以访问次数为 value,访问一次自增一次。当到了某个值的时候,比如每分钟超过 120 次就拒绝请求。

但是像这种计数器限流的问题就在于第 1 分钟和第 2 分钟的交界处会允许两倍的流量。比如第 60 秒来了 120 个请求,第 61 秒又来了 120 个请求,这种情况下计数器限流是拦不住的。固定窗口计数器只关心 "单个窗口内的流量是否超标",不关心 "相邻窗口的流量总和"

复制代码
-- KEYS[1] = 限流key(如 rate_limit:ip:192.168.1.1)
-- ARGV[1] = 限流时间窗口(秒)
-- ARGV[2] = 最大请求阈值

local current = redis.call('INCR', KEYS[1])

-- 首次设置过期时间
if current == 1 then
    redis.call('EXPIRE', KEYS[1], ARGV[1])
end

-- 检查是否超过阈值
if current > tonumber(ARGV[2]) then
    return 0 -- 超过阈值
end

return 1 -- 允许访问

hash

先哈希结构可以存对象,如果要分开去存储对象不同字段,要去精细修改某个对象的某个属性,用哈希是非常合适的。

第二,哈希结构还可以做购物车,以用户 id 为 key,商品 id 为 field,商品数据为 value,去搞一个购物车。

复制代码
# 用户id=1001

# 添加商品101,数量为1  
HSET cart:1001 101 1  

# 再添加商品102,数量为2:  
HSET cart:1001 102 2  

# 修改商品101的数量为3(直接设置):  
HSET cart:1001 101 3  

# 给商品101增加2个(此时变为5个):  
HINCRBY cart:1001 101 2  

# 获取商品101的数量  
HGET cart:1001 101  

# 获取所有商品  
HGETALL cart:1001  

# 删除商品102  
HDEL cart:1001 102  

# 清空购物车  
DEL cart:1001  

第三,哈希结构还可以做一个动态的配置管理,只能做一个简单的配置管理,比如做一些功能的开关之类的,或者做一个动态的参数管理,比如以某个 config 为 key,以功能为 field,以 true 或 false 为 value。这样就相当于把 REDIS 当成了一个简单的配置中心去做了。但 REDIS 毕竟是基于内存的,没那么可靠,而且配置的发布变更其实也没有权限管控,所以还是比较危险的,最好还是用 nacos 这种专业的配置中心。如果系统不想引入这么重的东西,只想用 redis 做一个简单的,也是完全 OK 的。

复制代码
# 开启搜索建议功能  
HSET global:config search_suggestions "true"  

# 默认使用暗黑模式的ui  
HMSET global:config ui_mode "dark"  

# 获取搜索建议功能的配置值  
HGET global:config search_suggestions  

# 关闭搜索建议功能  
HSET global:config search_suggestions "true"  

第四,它可以去做一种多维度的数据统计,比如记录不同地区的视频的访问量,以视频 id 为 key,以地区为 field,以访问次数为 value,访问一次就自增一次,就实现了不同地区的视频访问量的记录。

复制代码
# 初始化或增加特定地区的访问量  
# 比如当视频ID为12345的视频被北京用户访问一次  
HINCRBY video_views:12345 beijing 1  


# 获取ID为12345的视频北京地区的访问量  
HGET video_views:12345 beijing  


# 获取ID为12345的视频的所有地区访问量  
HGETALL video_views:12345  

list

首先,这个 list 可以实现一个简单的消息队列,消息的生产者直接 LPUSHpush 消息到这个 list 里面,消息的消费者使用 BRPOP 阻塞式的去读取这个数据,还要保证消息不被重复处理,保证消息的幂等性,就得给消息分配一个唯一 id,消息的消费者去保存这些处理过的消息 id,避免去做一个重复消费,list 去做一个简单的消息队列,其实整体上是能用的,但是没有办法做消息的持久化,没有办法去搞死信队列,也没有办法去让多个消费者去消费同一条消息。如果系统比较小,不想引用那么重的 MQ,用 redis list 做一个消息队列其实是可以满足基本需求的。如果 REDIS 的版本比较高,可以用 stream,stream 是一个 REDIS 高版本专门用来做消息队列的工具功能,功能更丰富一点,更像一个完整的消息队列一点。但是也是内存式的,没有办法去做持久化,所以还是和正式的消息队列相比还是有一点点缺陷,但是整体已经好很多了。如果不想引入一个重的 MQ,完全可以用 stream 或者用 list 去做。

复制代码
# 给消息队列插入数据  
LPUSH my_queue "{msg_id:1, type: 123, content: xxxx}"  


# 阻塞式消费队列数据  
BRPOP my_queue  

第二,list 的可以去做一个简单的实时的消息流,比如以用户的 id 为 key,以消息 id 为 value 去存入 list 里面。当某个人发帖子或者发朋友圈的时候,就给放到用户的 id 对应的 list 里面。当某个人去打开朋友圈,就从 list 去读取一些数据,显示到页面里。

复制代码
# 用户1001发了一条朋友圈,该朋友圈id=12345,将其推送给所有好友的列表  
# 假设用户1001的好友列表是[2001, 2002, 2003],可使用管道(pipeline)来批量操作  
LPUSH feed:2001 "12345"  
LPUSH feed:2002 "12345"  
LPUSH feed:2003 "12345"  


# 当用户2001打开朋友圈,获取前10条消息ID  
LRANGE feed:2001 0 9  

set

首先 set 可以做一个抽奖系统,直接把奖品 add 到 set 集合里面。

如果不允许重复中奖的场景,就直接 spop 随意的移除一个元素,这个元素就是用户的奖品。

如果是允许重复中奖的场景,就直接 srandmember 去随机获取一个元素,这个就是用户的奖品。

因为 set 无序无重复的特性,所以其实可以方便的去做一些数据去重的一些功能。sismember 查询命令的时间复杂度也是 0(1) 的,所以去做白名单判断数据是否在白名单里面也是非常不错的。

复制代码
# 添加奖品到奖池  
SADD prize_pool "iPhone15" "AirPods" "100元券" "50元券" "10元券" "谢谢参与"  


# 查看所有奖品  
SMEMBERS prize_pool  


# 查看奖品数量  
SCARD prize_pool  


# 用户抽奖(不允许重复中奖)  
SPOP prize_pool  


# 用户抽奖(允许重复中奖)  
SRANDMEMBER prize_pool  

set 还可以做点赞,以帖子或者朋友圈 id 为 key,以点赞用户的 id 为 value 去存入 set,谁点赞了就放入对应的 set 集合里面,做点赞也是非常不错的。

复制代码
# id为1001用户点赞了id为12345的帖子  
SADD post:12345:likes 1001  


# 取消点赞(从Set中移除用户ID)  
SREM post:12345:likes 1001  


# 获取所有点赞用户ID  
SMEMBERS post:12345:likes  


# 获取点赞数量  
SCARD post:12345:likes  


# 检查用户1001是否对帖子12345点过赞。返回1表示点过赞,0表示没有。  
SISMEMBER post:12345:likes 1001  

set 还可以做交集、并集、差集等,所以比较适合去做共同好友、共同关注、共同兴趣等等。共同好友就说以用户 id 为 key,以好友的 id 为 value,取交集就能获取到共同好友。

也可以去存用户的信息标签,以用户 id 为 key,以信息标签为 value,存入 set,不同用户去交集就能计算不同用户的共同兴趣。

复制代码
# 用户1001的兴趣标签  
SADD user:1001:tags "篮球" "电影" "编程" "旅行"  


# 用户1002的兴趣标签  
SADD user:1002:tags "电影" "美食" "摄影" "旅行"  


# 获取用户1001的所有标签  
SMEMBERS user:1001:tags  


# 检查用户是否对某标签感兴趣  
SISMEMBER user:1001:tags "编程"  


# 获取用户标签数量  
SCARD user:1001:tags  


# 计算两个用户的共同兴趣  
SINTER user:1001:tags user:1002:tags  


# 计算用户1001独有的兴趣  
SDIFF user:1001:tags user:1002:tags  

zset

zset 的特点就是有序、无重复,每个元素都有一个 score 去用于排序,比较适合需要排序的场景。

首先排序最常用的场景就是排行榜,比如积分排行榜或者步数排行榜,以积分作为 score,用户 id 为 value,存入到 zset 里面,使用 zrange 就能从小到大去获取 top n,使用 zrange 就能获取到从大到小的 top n。

复制代码
# 添加用户积分(用户不存在时添加,存在时更新)  
ZADD leaderboard 500 1001  
ZADD leaderboard 750 1002  
ZADD leaderboard 1200 1003  
...  


# 用户1001增加50积分  
ZINCRBY leaderboard 50 1001  


# 用户1002减少30积分  
ZINCRBY leaderboard -30 1002  


# 获取前10名(从高到低)  
ZREVRANGE leaderboard 0 9 WITHSCORES  


# 获取后10名(从低到高)  
ZRANGE leaderboard 0 9 WITHSCORES  


# 获取用户排名(从高到低)  
ZREVRANK leaderboard 1001  


# 获取用户积分  
ZSCORE leaderboard 1001  


# 获取排行榜总人数  
ZCARD leaderboard  


# 查询积分在800到1000的用户(示例)  
ZRANGEBYSCORE leaderboard 800 1000 WITHSCORES  

zset 也很适合做滑动窗口的限流,比如针对 IP 限流,用户 IP 为 key,以请求的时间戳为 score,请求的 id 为 Value 存入到 zset,这样就能统计一分钟之内某个 IP 的请求次数,超过值就进行限流。

复制代码
-- KEYS[1]: IP限流键(如 ip_requests:192.168.1.1)
-- ARGV[1]: 当前时间戳(毫秒)
-- ARGV[2]: 时间窗口(秒)
-- ARGV[3]: 最大请求数

local current_time = tonumber(ARGV[1])
local window_seconds = tonumber(ARGV[2])
local max_requests = tonumber(ARGV[3])

-- 计算窗口开始时间
local window_start = current_time - window_seconds * 1000

-- 清理过期请求
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, window_start)

-- 获取当前请求数
local request_count = redis.call('ZCARD', KEYS[1])

-- 检查是否超过阈值
if request_count >= max_requests then
    return 0 -- 限流
end

-- 添加新请求
redis.call('ZADD', KEYS[1], current_time, 'req:' .. redis.sha1(ARGV[1] .. math.random()))

-- 设置键过期时间(窗口时间+缓冲)
redis.call('EXPIRE', KEYS[1], window_seconds + 1)

return 1 -- 允许

zset 还能保存用户的浏览记录,比如以用户 id 为 key,以时间戳为 score,以这个帖子为 member,然后去放入到这个 zset 里面,然后就可以按照时间范围快速的去做分页查询了。

复制代码
# 用户1001浏览帖子12345(时间戳为1698652800)  
ZADD user:1001:history 1698652800 "post:12345"  


# 用户1001浏览帖子67890(时间戳为1698652900)  
ZADD user:1001:history 1698652900 "post:67890"  


# 获取最新10条浏览记录(按时间倒序)  
ZREVRANGE user:1001:history 0 9 WITHSCORES  


# 获取最早浏览记录(按时间正序)  
ZRANGE user:1001:history 0 9 WITHSCORES  


# 按时间范围查询(查询指定时间段内的浏览记录)  
ZRANGEBYSCORE user:1001:history 1698652800 1698653000 WITHSCORES  


# 分页查询  
## 第一页(每页10条)  
ZREVRANGE user:1001:history 0 9 WITHSCORES  

## 第二页  
ZREVRANGE user:1001:history 10 19 WITHSCORES  


# 删除指定时间前的记录  
ZREMRANGEBYSCORE user:1001:history -inf 1698652900  


# 清空浏览历史  
DEL user:1001:history  

第四,zset 还能实现在线的用户列表与用户活跃的时间戳为score,用户 id 为 value,存入 zset,然后就可以去做这种按登录时间排序,可以查询,可以踢人下线这种功能。

复制代码
# 用户1001在时间戳1698652800000登录/活跃  
ZADD online_users 1698652800000 "user:1001"  


# 获取最近活跃的前10名用户(降序)  
ZREVRANGE online_users 0 9 WITHSCORES  


# 获取最早活跃的前10名用户(升序)  
ZRANGE online_users 0 9 WITHSCORES  


# 获取用户1001最后活跃时间  
ZSCORE online_users 1001  


# 检查用户是否在线  
ZSCORE online_users 1001  # 返回时间戳表示在线,nil表示离线  


# 移除用户,强制下线  
ZREM online_users 1001  


# 清理1小时未活跃的用户(当前时间戳1698654000000)  
ZREMRANGEBYSCORE online_users -inf (1698654000000-3600000)