开启生长之旅!这是我参加「日新计划 2 月更文挑战」的第 6 天,点击查看活动概况

一、概述

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

二、Redis常见运用场景

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

三、Redis有哪些数据结构

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

3.1 String字符串

字符串典型的运用场景:

  • 单值缓存
  • 目标缓存
  • 计数器
  • 分布式锁

单值缓存

127.0.0.1:6379> set num 1
OK
127.0.0.1:6379> get num
"1"
127.0.0.1:6379>

目标缓存

SET user:1 value(json格局数据)

计数器

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

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

127.0.0.1:6379> incr article:read:id1
(integer) 1
127.0.0.1:6379> incr article:read:id1
(integer) 2
127.0.0.1:6379> incr article:up:id1
(integer) 1
127.0.0.1:6379> incr article:up:id2
(integer) 1
127.0.0.1:6379> incr article:comment:id1
(integer) 1
127.0.0.1:6379> incr article:comment:id1
(integer) 2
127.0.0.1:6379>

分布式锁

  • setnx

守时使命防止同一时刻重复履行,能够在事务履行代码前运用分布式锁控制。

127.0.0.1:6379> setnx job GlobalNotifyJob
(integer) 1
127.0.0.1:6379> get job
"GlobalNotifyJob"
127.0.0.1:6379> ttl job
(integer) -1
127.0.0.1:6379>

伪代码如下:

@Slf4j
@Component
public 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 value1
OK
127.0.0.1:6379> ttl key1
(integer) 53
127.0.0.1:6379> get key1
"value1"
127.0.0.1:6379>

涉及到 Redis分布式锁 知识点能够参考博主之前发的文章:讨论Redis分布式锁处理优惠券拼抢问题

3.2 hash哈希

哈希典型运用场景:

  • 缓存目标信息(帖子标题、摘要、作者信息)
  • 记载帖子的点赞数、谈论数和点击数
  • 电商购物车
指令 描绘
HSET key field value 存储一个哈希表key的键值
HSETNX key field value 存储一个不存储的哈希表key的键值
HMSET key field value [field value…] 在一个哈希表key中存储多个键值对
HGET key field value 获取哈希表key对应的field键值
HMGET key field value 批量获取哈希表key中多个field键值
HDEL key field [field …] 删去哈希表key中多个field的键值
HLEN key 回来哈希表key中field的数量
HGETALL key 回来哈希表key中一切的键值
127.0.0.1:6379> hmset user:1 name austin age 25 address guangzhou balance 6888
OK
127.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 address
1) "25"
2) "guangzhou"
127.0.0.1:6379> hlen user:1
(integer) 4
127.0.0.1:6379> hgetall user:1
1) "name"
2) "austin"
3) "age"
4) "25"
5) "address"
6) "guangzhou"
7) "balance"
8) "6888"
127.0.0.1:6379>

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

3.3 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前后刺进pivot
LREM 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,一向堵塞等候

3.4 set调集

列表的典型运用场景:

  • 抽奖
  • 微博点赞,保藏,标签
  • 一起老友

抽奖场景:

  1. 用户参加抽奖
# 将用户10001参加产品a的参加池子中
SADD luckdraw:product:a 10001
  1. 查看参加产品a抽奖的一切用户
SMEMBERS luckdraw:product:a
  1. 抽取1名走运中奖者
SPOP luckdraw:product:a 1

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

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

点赞场景:

// 博客点赞功用需求:
1. 同一用户一篇博客只能点赞一次,再次点赞为撤销点赞
2. 假如当时用户现已点赞,则点赞按钮高亮显现(前端完成,依据回来的isLike字段属性做判别)

具体的功用完成过程:

  1. Post帖子信息表新增一个isLike字段,标识是否有被当时用户点赞
  2. 修正点赞功用,使用Redisset调集判别是否点赞过,未点赞过的点赞数+1,已点赞过大点赞数-1
  3. 修正依据帖子ID查询帖子信息的事务,判别当时登录用户是否现已点赞过,赋值isLike字段回来给前端
  4. 修正分页查询帖子的事务,判别当时用户是否点赞过,赋值isLike字段回来给前端

伪代码完成:

@Override
public Result likePost(Long id, Long currentUserId) {
    // 1.判别当时用户是否现已点赞
    String key = "post:liked:" + id;
    Post post = this.getById(id);
    if (post == null) {
        return Result.fail("post not found!");
    }
    Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, currentUserId);
    if (BooleanUtil.isFalse(isMember)) {
        // 假如未点赞,数据库帖子点赞数加1
        post.setLikeCount(post.getLikeCount() + 1);
        boolean success = this.update(post);
        if (success) {
            // 保存用户点赞记载到Redis的set调集中
            stringRedisTemplate.opsForSet().add(key, currentUserId.toString());
        }
    } else {
        // 假如现已点赞,撤销点赞,数据库帖子点赞数-1
        post.setLikeCount(post.getLikeCount() - 1);
        boolean success = this.update(post);
        if (success) {
            // 移除set调集中的用户点赞记载
            stringRedisTemplate.opsForSet().remove(key, currentUserId.toString());
        }
    }
    return Result.succ();
}

在回来帖子概况和列表事务中,需求判别当时用户是否点赞过:

/**
 * 在回来帖子概况和列表事务中,需求判别当时用户是否点赞过
 */
private PostVO isPostLiked(PostVO postVO, Long currentUserId) {
    String key = "post:liked:" + postVO.getId();
    Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, currentUserId);
    postVO.setIsLiked(isMember);
    return postVO;
}

一起老友场景:

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

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

取交集,获取用户1和用户2的一起老友,为用户4。

127.0.0.1:6379> sadd user_1 2 3 4
(integer) 3
127.0.0.1:6379> sadd user_2 4 5 7
(integer) 3
127.0.0.1:6379> sinter user_1 user_2
1) "4"
127.0.0.1:6379>

sorted set有序调集

列表的典型运用场景:

  • 微博热搜榜
  • 刷礼物实时排行榜
  • 博客社区本周热议

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

zset几个基本指令:

指令 阐明
zrange key start stop [WITHSCORES] 将调集元素依照次序值升序排序再输出,startstop约束遍历的约束规模
zincrby key increment member 有序集key的成员memberscore值加上增量increment
ZUNIONSTORE destination numkeys key [key …] [WEIGHTS weight [weight …]] 核算给定的一个或多个有序集的并集,其间给定key的数量必须以numkeys参数指定,并将该并集 (成果集) 贮存到destination
127.0.0.1:6379[3]> zadd zsetofpost 89 post:1
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 123 post:2
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 32 post:3
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 432 post:4
(integer) 1
127.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 天之后就主动删去缓存。

画图剖析:

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

终究完成效果:

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

Bitmaps位图

位图的典型运用场景:

  • 用户接连报到功用

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

很多社区、博客渠道其实都有每日报到模块,一开端看到这个模块需求的时分,很多人榜首反应是使用MySQL来完成,创立一个报到表,记载用户ID和报到时刻,然后核算的时分从数据库中取出来然后聚合核算,这样规划其实存在弊端,如咱们想要做一些杂乱的功用就不是太方便了,或者说不是太高功用了,比如,今天是接连报到的第几天,在一守时刻内接连报到了多少天。别的一方面,假如按 100 万用户量级来核算,一个用户每年能够发生 365 条记载,100 万用户的一切报到记载那就有点恐惧了,查询核算速度也会越来越慢。其实RedisBitmaps位图操作十分适合处理每日报到功用场景,因为Bit的值为0或者1,位图的每一位代表一天的报到,1表明已签,0表明未签。 考虑到每月初需求重置接连报到次数,最简略的方式是按用户每月存一条报到数据(也能够每年存一条数据)。Key的格局为u:sign:uid:yyyyMM,Value则选用长度为4个字节(32位)的位图(最大月份只有31天)。

Redis位图指令基本指令

指令 阐明
SETBIT key offset value 对key所贮存的字符串值,设置或清除指定偏移量上的位(bit)
BITPOS key bit [start] [end] 查询指定字节区间榜首个被设置成1的bit位的方位
GETBIT key offset 查询指定偏移方位的bit值
BITCOUNT key [start end] 核算指定字节区间bit为1的数量
GETBIT key offset 查询指定偏移方位的bit值
BITFIELD key offset 查询指定偏移方位的bit值

这里的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相当于主题。

具体 Redis发布订阅介绍 能够参考博主之前发的文章:Redis发布订阅实践场景和完成

总结

本文具体介绍了Redis的五种数据结构和运用场景,希望能够协助咱们处理实践工作中遇到的问题,后续还会陆续出Redis相关的文章,如有协助,感谢点赞+关注✔+保藏❤,我是‍ austin流川枫,咱们下期见!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。