写点什么

一口气讲完了 Redis 常用的数据结构及应用场景

作者:小小怪下士
  • 2023-02-07
    湖南
  • 本文字数:6678 字

    阅读完需:约 22 分钟

一、概述

Redis是互联网技术领域使用最为广泛的存储中间件,它是Remote Dictionary Service(远程字典服务)的首字母缩写,Redis以其超高的性能、活跃的社区、详细的文档以及丰富的客户端库支持在开源中间件领域广受好评,国内外很多大型互联网都在使用Redis,比如:TwitterGithub、新浪微博、阿里巴巴、京东、Stack Overflow等,可以说,深入了解Redis应用和实践,已成为如今中高级后端加法绕不开的必备技能。

二、Redis 常见应用场景

三、Redis 有哪些数据结构

3.1 String 字符串

🔥字符串典型的使用场景:


  • 单值缓存

  • 对象缓存

  • 计数器

  • 分布式锁


单值缓存


127.0.0.1:6379> set num 1OK127.0.0.1:6379> get num"1"127.0.0.1:6379>
复制代码


单值缓存


SET user:1 value(json格式数据)
复制代码


计数器


文章阅读量、点赞量、评论量



127.0.0.1:6379> incr article:read:id1(integer) 1127.0.0.1:6379> incr article:read:id1(integer) 2127.0.0.1:6379> incr article:up:id1(integer) 1127.0.0.1:6379> incr article:up:id2(integer) 1127.0.0.1:6379> incr article:comment:id1(integer) 1127.0.0.1:6379> incr article:comment:id1(integer) 2127.0.0.1:6379>
复制代码


分布式锁


  • setnx


定时任务防止同一时刻重复执行,可以在业务执行代码前使用分布式锁控制。


127.0.0.1:6379> setnx job GlobalNotifyJob(integer) 1127.0.0.1:6379> get job"GlobalNotifyJob"127.0.0.1:6379> ttl job(integer) -1127.0.0.1:6379>
复制代码


伪代码如下:


@Slf4j@Componentpublic class GlobalNotifyJob {
private static final String LOCK_KEY = "redis_notify_lock";
/** * 每小时执行一次 */ @Scheduled(cron = "0 0 0/1 * * ?") public void notify() { if (!lockService.grabLock(LOCK_KEY)) { log.info("[GlobalNotifyJob] 没有拿到锁, 停止操作......"); return; } // 拿到锁,开始执行业务... }}
复制代码


  • setex + 过期时间【SETNX KEY_NAME TIMEOUT VALUE】


127.0.0.1:6379> setex key1 60 value1OK127.0.0.1:6379> ttl key1(integer) 53127.0.0.1:6379> get key1"value1"127.0.0.1:6379>
复制代码

hash 哈希

🔥哈希典型应用场景:


  • 缓存对象信息(帖子标题、摘要、作者信息)

  • 记录帖子的点赞数、评论数和点击数

  • 电商购物车



127.0.0.1:6379> hmset user:1 name austin age 25 address guangzhou balance 6888OK127.0.0.1:6379> hget user:1 name"austin"127.0.0.1:6379> hget user:1 balance"6888"127.0.0.1:6379> hmget user:1 age address1) "25"2) "guangzhou"127.0.0.1:6379> hlen user:1(integer) 4127.0.0.1:6379> hgetall user:11) "name"2) "austin"3) "age"4) "25"5) "address"6) "guangzhou"7) "balance"8) "6888"127.0.0.1:6379>
复制代码


list 列表

🔥列表的典型应用场景:


  • 文章列表

  • 微博和微信公众号消息


Stack(栈FILO) = LPUSH + LPOP Queue(队列FIFO)= LPUSH + RPOP Blocking MQ(阻塞队列)= LPUSH + BRPOP
LPUSH key value [value ...] // 将一个或多个值value插入到key列表的表头(最左边)RPUSH key value [value ...] // 将一个或多个值value插入到key列表的表尾(最右边)LPOP key // 移除并返回key列表的头元素RPOP key // 移除并返回key列表的尾元素LRANGE key start stop // 返回列表key中指定区间内的元素,区间以偏移量start和stop指定LINSERT key BEFORE|AFTER pivot element // 在元素element前后插入pivotLREM key count element //根据参数 COUNT 的值,移除列表中与参数 VALUE 相等的元素 count > 0 : 从表头开始向表尾搜索,移除与 VALUE 相等的元素,数量为 COUNT BLPOP key [key ...] timeout //从key列表表头弹出一个元素,若列表中没有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待BRPOP key [key ...] timeout //从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待
复制代码

set 集合

🔥列表的典型应用场景:


  • 抽奖

  • 微博点赞,收藏,标签

  • 共同好友


抽奖场景:


  1. 用户参与抽奖


# 将用户10001加入商品a的参与池子中SADD luckdraw:product:a 10001
复制代码


  1. 查看参与商品 a 抽奖的所有用户


SMEMBERS luckdraw:product:a
复制代码


  1. 抽取 1 名幸运中奖者


SPOP luckdraw:product:a 1
复制代码




共同好友场景:



用户 1 的好友为:3,4,8 用户 2 的好友为:4,5,11


取交集,获取用户 1 和用户 2 的共同好友,为用户 4。


127.0.0.1:6379> sadd user_1 2 3 4(integer) 3127.0.0.1:6379> sadd user_2 4 5 7(integer) 3127.0.0.1:6379> sinter user_1 user_21) "4"127.0.0.1:6379>
复制代码

sorted set 有序集合

🔥列表的典型应用场景:


  • 微博热搜榜

  • 刷礼物实时排行榜

  • 博客社区本周热议


Redis有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double类型的分数,Redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数score却可以重复。下面使用redis-cli实践Redis有序集合命令:


zset 几个基本命令:



127.0.0.1:6379[3]> zadd zsetofpost 89 post:1(integer) 1127.0.0.1:6379[3]> zadd zsetofpost 123 post:2(integer) 1127.0.0.1:6379[3]> zadd zsetofpost 32 post:3(integer) 1127.0.0.1:6379[3]> zadd zsetofpost 432 post:4(integer) 1127.0.0.1:6379[3]> zadd zsetofpost 128 post:5(integer) 1
#升序排序127.0.0.1:6379[3]> zrange zsetofpost 0 -1 withscores 1) "post:3" 2) "32" 3) "post:1" 4) "89" 5) "post:2" 6) "123" 7) "post:5" 8) "128" 9) "post:4"10) "432"
#降序排序127.0.0.1:6379[3]> zrevrange zsetofpost 0 -1 withscores 1) "post:4" 2) "432" 3) "post:5" 4) "128" 5) "post:2" 6) "123" 7) "post:1" 8) "89" 9) "post:3"10) "32"
#有序集合某个元素的score值加上对应的增量127.0.0.1:6379[3]> zincrby zsetofpost 40 post:1"129"127.0.0.1:6379[3]> zincrby zsetofpost 500 post:3"532"127.0.0.1:6379[3]> zrange zsetofpost 0 -1 withscores 1) "post:2" 2) "123" 3) "post:5" 4) "128" 5) "post:1" 6) "129" 7) "post:4" 8) "432" 9) "post:3"10) "532"
复制代码


简单认识了Redis有序集合和对应的命令之后,我们来实现本周热议排行榜功能,博客的本周热议主要的实现思路是:


  1. 库获取最近 7 天的所有文章(或者加多一个条件:评论数量大于 0)。

  2. 把文章的评论数量作为有序集合的分数score,文章的 ID 作为key存储到zset中,当有人发表评论的时候,直接使用命令加一,并重新计算得到排行榜。

  3. 本周热议上有标题和评论数量,因此,我们还需要把文章的基本信息存储到Redis中,这样得到文章的 ID 之后,我们再从缓存中得到标题等信息,这里我们可以使用hash的结构来存储文章的信息。

  4. 因为是本周热议,如果文章发表超过 7 天了之后就会失效,所以我们可以给文章的有序集合一个有效时间。超过 7 天之后就自动删除缓存。


画图分析:



最终实现效果:


Bitmaps 位图

🔥位图的典型应用场景:


  • 用户连续签到功能



很多社区、博客平台其实都有每日签到模块,一开始看到这个模块需求的时候,很多人第一反应是利用MySQL来实现,创建一个签到表,记录用户 ID 和签到时间,然后统计的时候从数据库中取出来然后聚合计算,这样设计其实存在弊端,如我们想要做一些复杂的功能就不是太方便了,或者说不是太高性能了,比如,今天是连续签到的第几天,在一定时间内连续签到了多少天。另外一方面,如果按 100 万用户量级来计算,一个用户每年可以产生 365 条记录,100 万用户的所有签到记录那就有点恐怖了,查询计算速度也会越来越慢。其实RedisBitmaps位图操作非常适合处理每日签到功能场景,因为 Bit 的值为 0 或者 1,位图的每一位代表一天的签到,1 表示已签,0 表示未签。 考虑到每月初需要重置连续签到次数,最简单的方式是按用户每月存一条签到数据(也可以每年存一条数据)。Key 的格式为u:sign:uid:yyyyMM,Value 则采用长度为 4 个字节(32 位)的位图(最大月份只有 31 天)。


Redis 位图命令基本命令



这里的 offset,大家姑且当做用户 ID 来看就可以了,那么究竟如何去实现用户打卡功能呢,我们可以利用上面的setbit命令来实现,setbit的作用说的直白就是:在你想要的位置操作字节值,比如说u:sign:1000:202302表示ID=1000的用户在2023年2月7号签到记录。


# 用户1000在2023年2月7号签到SETBIT u:sign:1000:202302 6 1 # 偏移量是从0开始,所以要把7减1
# 检查用户1000在2023年2月7号是否签到GETBIT u:sign:1000:202302 6 # 偏移量是从0开始,所以要把7减1
# 统计用户1000在2月份签到次数BITCOUNT u:sign:1000:202302
# 获取2月份前28天的签到数据BITFIELD u:sign:1000:202302 get u28 0
# 获取2月份首次签到日期BITPOS u:sign:1000:202302 1 # 返回的首次签到的偏移量,加上1即为当月的某一天
复制代码


示例代码:


/** * 基于Redis位图的用户签到功能工具实现类 * * @author: austin * @since: 2023/2/7 1:50 */public class UserSignKit {
private Jedis jedis = new Jedis();
/** * 用户签到 * * @param uid 用户ID * @param date 日期 * @return 之前的签到状态 */ public boolean doSign(int uid, LocalDate date) { int offset = date.getDayOfMonth() - 1; return jedis.setbit(buildSignKey(uid, date), offset, true); }
/** * 检查用户是否签到 * * @param uid 用户ID * @param date 日期 * @return 当前的签到状态 */ public boolean checkSign(int uid, LocalDate date) { int offset = date.getDayOfMonth() - 1; return jedis.getbit(buildSignKey(uid, date), offset); }
/** * 获取用户签到次数 * * @param uid 用户ID * @param date 日期 * @return 当前的签到次数 */ public long getSignCount(int uid, LocalDate date) { return jedis.bitcount(buildSignKey(uid, date)); }
/** * 获取当月连续签到次数 * * @param uid 用户ID * @param date 日期 * @return 当月连续签到次数 */ public long getContinuousSignCount(int uid, LocalDate date) { int signCount = 0; String type = String.format("u%d", date.getDayOfMonth()); List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0"); if (list != null && list.size() > 0) { // 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况 long v = list.get(0) == null ? 0 : list.get(0); for (int i = 0; i < date.getDayOfMonth(); i++) { if (v >> 1 << 1 == v) { // 低位为0且非当天说明连续签到中断了 if (i > 0) { break; } } else { signCount += 1; } v >>= 1; } } return signCount; }
/** * 获取当月首次签到日期 * * @param uid 用户ID * @param date 日期 * @return 首次签到日期 */ public LocalDate getFirstSignDate(int uid, LocalDate date) { long pos = jedis.bitpos(buildSignKey(uid, date), true); return pos < 0 ? null : date.withDayOfMonth((int) (pos + 1)); }
/** * 获取当月签到情况 * * @param uid 用户ID * @param date 日期 * @return Key为签到日期,Value为签到状态的Map */ public Map<String, Boolean> getSignInfo(int uid, LocalDate date) { Map<String, Boolean> signMap = new HashMap<>(date.getDayOfMonth()); String type = String.format("u%d", date.lengthOfMonth()); List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0"); if (list != null && list.size() > 0) { // 由低位到高位,为0表示未签,为1表示已签 long v = list.get(0) == null ? 0 : list.get(0); for (int i = date.lengthOfMonth(); i > 0; i--) { LocalDate d = date.withDayOfMonth(i); signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v); v >>= 1; } } return signMap; }
private static String formatDate(LocalDate date) { return formatDate(date, "yyyyMM"); }
private static String formatDate(LocalDate date, String pattern) { return date.format(DateTimeFormatter.ofPattern(pattern)); }
private static String buildSignKey(int uid, LocalDate date) { return String.format("u:sign:%d:%s", uid, formatDate(date)); }
public static void main(String[] args) { UserSignKit kit = new UserSignKit(); LocalDate today = LocalDate.now();
{ // doSign boolean signed = kit.doSign(1000, today); if (signed) { System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd")); } else { System.out.println("签到完成:" + formatDate(today, "yyyy-MM-dd")); } }
{ // checkSign boolean signed = kit.checkSign(1000, today); if (signed) { System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd")); } else { System.out.println("尚未签到:" + formatDate(today, "yyyy-MM-dd")); } }
{ // getSignCount long count = kit.getSignCount(1000, today); System.out.println("本月签到次数:" + count); }
{ // getContinuousSignCount long count = kit.getContinuousSignCount(1000, today); System.out.println("连续签到次数:" + count); }
{ // getFirstSignDate LocalDate date = kit.getFirstSignDate(1000, today); System.out.println("本月首次签到:" + formatDate(date, "yyyy-MM-dd")); }
{ // getSignInfo System.out.println("当月签到情况:"); Map<String, Boolean> signInfo = new TreeMap<>(kit.getSignInfo(1000, today)); for (Map.Entry<String, Boolean> entry : signInfo.entrySet()) { System.out.println(entry.getKey() + ": " + (entry.getValue() ? "√" : "-")); } } }}
复制代码


运行结果:


您已签到:2023-02-07您已签到:2023-02-07本月签到次数:5连续签到次数:3本月首次签到:2023-02-02当月签到情况:2023-02-01: -2023-02-02: √2023-02-03: √2023-02-04: √2023-02-05: -2023-02-06: √2023-02-07: √2023-02-08: -2023-02-09: -2023-02-10: -2023-02-11: -2023-02-12: -2023-02-13: -2023-02-14: -2023-02-15: -2023-02-16: -2023-02-17: -2023-02-18: -2023-02-19: -2023-02-20: -2023-02-21: -2023-02-22: -2023-02-23: -2023-02-24: -2023-02-25: -2023-02-26: -2023-02-27: -2023-02-28: -
复制代码

Redis 发布订阅

Redis 提供了发布订阅功能,可以用于消息的传输,Redis 的发布订阅机制包括三个部分:发布者订阅者Channel。发布者和订阅者都是 Redis 客户端,Channel 则为 Redis 服务器端,发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。Redis 的这种发布订阅机制与基于主题的发布订阅类似,Channel 相当于主题。

用户头像

还未添加个人签名 2022-09-04 加入

热衷于分享java技术,一起交流学习,探讨技术。 需要Java相关资料的可以+v:xiaoyanya_1

评论

发布
暂无评论
一口气讲完了Redis常用的数据结构及应用场景_Java_小小怪下士_InfoQ写作社区