前文,我们讨论过redis 的数据结构及使用场景。可参考:

参考: 总结篇4:redis 核心数据存储结构及核心业务模型实现应用场景 https://www.cnblogs.com/yizhiamumu/p/16566540.html

一:zset(sorted set:有序集合)

Redis zset和Set一样也是String类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。Redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。

Redis zset类似Java里的LinkedSet(不重复有序),当zset中最后一个value 被移除后,数据结构自动删除,内存被回收。

常用操作
zadd key score member [[score member]...] // 往有序集合key 中加入带分值元素
zrem key member [member ...] // 从有序集合中删除元素
zcore key member // 返回有序集合key 中元素member 的分值
zincrby key increment member // 为有序集合key 中元素member的分值加上increment
zcard key // 返回有序集合key 中元素个数
zrange key start stop [withscores] // 正序获取有序集合key 从start 下标到stop 下标的元素Zrevrange key start stop [withscores] // 倒序 集合操作
zunionstore destkey numkeys key [key ...] 并集计算
zinterstore destkey numkeys key [key ...] 交集计算

应用场景

1 点击新闻
zincrby hotnews:202008 1 name
2 单日排行榜
zrevrange hotnews:202208 0 9 withscores
3 七日搜索榜单计算
zunionstore hotnews:20220801-20220807 7
4 展示七日榜前十
zrevrange hotnews:20220801-20220807 0 9 withscores

二:zset 应用场景

因为Rediszset底层的数据结构是skipList,最底层链表有序,所有可以有以下使用场景:

1. 延时队列

score作为时间戳,自动按照时间最近的进行排序,启一个线程持续poll并设置park时间,完成延迟队列的设计,可参考Executors.newScheduledThreadPool中的DelayedWorkQueue

2. 排行榜

score作为浏览次数,自动进行排序,但要注意冷数据。

3. 滑动窗口限流

score作为时间戳,可统计最近一段时间内内的成员数量,实现滑动窗口限流。

下面我们简单实现一下应用场景。

三:代码实现

1. 延时队列

zset 会按 score 进行排序,如果 score 代表想要执行时间的时间戳。在某个时间将它插入zset集合中,它变会按照时间戳大小进行排序,也就是对执行时间前后进行排序。

起一个死循环线程不断地进行取第一个key值,如果当前时间戳大于等于该key值的score就将它取出来进行消费删除,可以达到延时执行的目的。

发送消息

代码如下:

public void sendMessage(long messageId, String message) {
System.out.println("发送消息");
Jedis client = jedisPool.getResource();
Pipeline pipeline = client.pipelined();
// score 设置成当前时间戳 + 延迟时间
pipeline.zadd(DELAY_QUEUE, System.currentTimeMillis() + DELAY_TIME * 1000,
String.format(MEMBER_PREFIX, messageId));
Map<String, String> map = new HashMap<>();
map.put(String.format(MEMBER_PREFIX, messageId), message);
pipeline.hset(DELAY_MESSAGE, map);
pipeline.syncAndReturnAll();
pipeline.close();
client.close();
System.out.println("======发送消息 over");
}

采用 pipeline 的方式,同时写入 zset 和 hash 中

消费消息

代码如下:

public void consumer() {
System.out.println("======消费消息开始");
Jedis client = jedisPool.getResource();
Set<Tuple> tupleSet = client.zrangeByScoreWithScores(DELAY_QUEUE, 0, System.currentTimeMillis());
for (Tuple t : tupleSet) {
long messageId = Long.valueOf(t.getElement().replaceAll("[^0-9]", ""));
messageHandler(messageId);
}
client.close();
System.out.println("------消费消息 over");
}
public void messageHandler(long messageId) {
System.out.println("===---===");
pool.execute(() -> { // 放到线程池处理
Jedis client = jedisPool.getResource();
String message = client.hget(DELAY_MESSAGE, String.format(MEMBER_PREFIX, messageId));
System.out.println("处理消息体" + message);
System.out.println("处理消息体成功");
Pipeline pipeline = client.pipelined();
pipeline.multi();
pipeline.hdel(DELAY_MESSAGE, String.format(MEMBER_PREFIX, messageId));
pipeline.zrank(DELAY_QUEUE, String.format(MEMBER_PREFIX, messageId));
pipeline.exec();
pipeline.close();
client.close();
});
}

问题

没有 ack 机制,当消费失败的情况下队列如何处理?

这是 topic 模式,广播模式如何搞?

2. 排行榜

经常浏览技术社区的话,应该对 “1小时最热门” 这类榜单不陌生。如何实现呢?

如果记录在数据库中,不太容易对实时统计数据做区分。我们以当前小时的时间戳作为 zset 的 key,把贴子ID作为 member ,点击数评论数等作为 score,当 score 发生变化时更新 score。利用 ZREVRANGE 或者 ZRANGE 查到对应数量的记录。

记录回复数

代码如下:

/**
* 模拟每次针对贴子的回复数加 1
*
* @param id
*/
public void post(long id) {
Jedis client = jedisPool.getResource();
client.zincrby(POSTLIST, 1, String.format(MEMBER_PREFIX, id));
client.close();
}

获取列表

代码如下:

/**
* 获取 Top 的贴子列表 ID
*
* @param size
* @return
*/
public List<Integer> getTopList(int size) {
List<Integer> result = new ArrayList<>();
if (size <= 0 || size > 100) {
return result;
}
Jedis client = jedisPool.getResource();
Set<Tuple> tupleSet = client.zrevrangeWithScores(POSTLIST, 0, size - 1);
client.close();
for (Tuple tuple : tupleSet) {
String t = tuple.getElement().replaceAll("[^0-9]", "");
result.add(Integer.valueOf(t));
}
return result;
}

模拟用户发帖的行为

代码如下:

public void test() throws InterruptedException {
int threadSize = 200;
long[] ids = {100, 102, 103, 104, 105, 106, 101, 108, 107, 200, 109, 201, 202};
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
pool.execute(() -> {
for (int j = 0; j < 3; j++) {
Random r = new Random();
int index = (int) (r.nextDouble() * ids.length);
post(ids[index]);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
}

问题

数量过大时会占用大量内存,需要清理很多冷数据

适合处理点击数、访问量之类,处理发帖回复这种还需要考虑,帖子审核不通过的情况

3. 限流策略

滑动窗口是限流常见的一种策略。如果我们把一个用户的 ID 作为 key 来定义一个 zset ,member 或者 score 都为访问时的时间戳。我们只需统计某个 key 下在指定时间戳区间内的个数,就能得到这个用户滑动窗口内访问频次,与最大通过次数比较,来决定是否允许通过。

代码如下:

/**
*
* @param userId
* @param period 窗口大小
* @param maxCount 最大频次限制
* @return
*/
public boolean isActionAllowed(String userId, int period, int maxCount) {
String key = String.format(KEY, userId);
long nowTs = System.currentTimeMillis();
Jedis client = jedisPool.getResource();
Pipeline pipe = client.pipelined();
pipe.multi();
pipe.zadd(key, nowTs, String.format(MEMBER, userId, nowTs));
pipe.zremrangeByScore(key, 0, nowTs - period * 1000);
Response<Long> count = pipe.zcard(key);
pipe.expire(key, period + 1);
pipe.exec();
pipe.close();
client.close();
return count.get() <= maxCount;
}

思路是每一个请求到来时,将时间窗口外的记录全部清理掉,只保留窗口内的记录。zset 中只有 score 值非常重要,value 值没有特别的意义,只需要保证它是唯一的就可以了

问题

  • 需要清理额外的数据

  • 限制的请求量过大时,会占用大量内存

redis zset 使用场景的更多相关文章

  1. redis的适应场景

    redis应用场景: 1.对数据高并发读写 2.对海量数据的高效存储和访问 3.对数据的高可扩展性和高可用性 做分布式扩展很简单,因为没有固定的表结构 redis介绍: redis是一个key-val ...

  2. redis 发展史 应用场景

    引言 在Web应用发展的初期,那时关系型数据库受到了较为广泛的关注和应用, 原因是因为那时候Web站点基本上访问和并发不高.交互也较少. 而在后来,随着访问量的提升,使用关系型数据库的Web站点多多少 ...

  3. redis系列-redis的使用场景

    redis越来越受大家欢迎,提升下速度,做下缓存,完成KPI之利器呀.翻译一篇文章<<How to take advantage of Redis just adding it to yo ...

  4. Redis 数据结构使用场景

    转自http://get.ftqq.com/523.get 一.redis 数据结构使用场景 原来看过 redisbook 这本书,对 redis 的基本功能都已经熟悉了,从上周开始看 redis 的 ...

  5. Lua脚本在redis分布式锁场景的运用

    目录 锁和分布式锁 锁是什么? 为什么需要锁? Java中的锁 分布式锁 redis 如何实现加锁 锁超时 retry redis 如何释放锁 不该释放的锁 通过Lua脚本实现锁释放 用redis做分 ...

  6. redis的使用场景和基本数据类型

    一:redis使用的场景 redis是一个高性能的NoSQL数据库,特点是高性能,持久存储,适应高并发的应用场景. 下面看看它的使用场景1.取最新N个数据的操作比如取网站的最新文章,通过下面方式,我们 ...

  7. redis作为缓存场景使用,内存耗尽时,突然出现大量的逐出,在这个逐出的过程中阻塞正常的读写请求,导致 redis 短时间不可用

    redis 突然大量逐出导致读写请求block   内容目录: 现象 背景 原因 解决方案 ref 现象 redis作为缓存场景使用,内存耗尽时,突然出现大量的逐出,在这个逐出的过程中阻塞正常的读写请 ...

  8. 实现排行榜神器——redis zset

    需求:假如现在需要搞个 “运动消耗卡路里排行榜”,例似微信步数排名,显示排名前20人的信息和消耗的卡里路,怎样实现排序? 一般思路:存储信息,然后数据库查询,排序?(假如有几十万人参与排名,这样查my ...

  9. redis的使用场景和优缺点

    使用场景和优缺点: 2 Redis用来做什么? 通常局限点来说,Redis也以消息队列的形式存在,作为内嵌的List存在,满足实时的高并发需求.而通常在一个电商类型的数据处理过程之中,有关商品,热销, ...

  10. 延时任务-基于redis zset的完整实现

    所谓的延时任务给大家举个例子:你买了一张火车票,必须在30分钟之内付款,否则该订单被自动取消.订单30分钟不付款自动取消,这个任务就是一个延时任务. 我之前已经写过2篇关于延时任务的文章: <完 ...

随机推荐

  1. Java子类是否能继承父类上的注解

    子类可以继承到父类上的注解吗? 在编写自定义注解时,可以通过指定@Inherited注解,申明自定义注解是否可以被继承:@Inherited只能实现类上的注解继承. 实现情况可细分为以下几种 未申明@ ...

  2. SpringBoot配置文件的优先级

    配置文件优先级 (1)命令行参数: (2)java:comp/env的JNDI属性(当前J2EE应用的环境): (3)JAVA系统的环境属性: (4)操作系统的环境变量: (5)JAR包外部的appl ...

  3. Swift开发基础05-可选项

    可选项定义 可选项,一般也叫可选类型,它允许将值设置为nil 在类型名称后面加个问号? 来定义一个可选项 var name: String? = "Jack" name = nil ...

  4. Django+forms+html

    在Django中,Form类通常通过继承django.forms.Form或django.forms.ModelForm来定义.当你定义一个表单类时,通常使用Form或ModelForm类,并使用各种 ...

  5. HINT: It seems you set a fixed date / time / datetime value as default for this field. This may not be what you want. If you want to have the current date as default, use `django.utils.timezone.now`

    WARNINGS: customers.PackingHead.packing_date: (fields.W161) Fixed default value provided. HINT: It s ...

  6. [oeasy]python0123_中文字符_文字编码_gb2312_激光照排技术_王选

    中文编码GB2312 回忆上次内容 上次回顾了 日韩各有 编码格式 日本 有假名 五十音 一字节 可以勉强放下   有日本汉字 字符数量超过20000+     韩国 有谚文 数量超过500 一个字节 ...

  7. oeasy教您玩转vim - 17 - # 向上向下

    向上向下 回忆上节课内容 和 f 相关的是跳到 向 前 跳到是 f 向 后 跳到是 F 和 t 相关的是贴靠 向 前 贴靠是 t 向 后 贴靠是 T 和 小写 相关的是 向前 向前 跳跃是 f 向前 ...

  8. [oeasy]python0040_换行与回车的不同_通用换行符_universal_newlines

    换行回车 回忆上次内容 区分概念 terminal终端 主机网络中 最终的 端点 TeleTYpewriter 电传打印机 终端硬件 shell 终端硬件基础上的 软件壳子 Console 控制台 主 ...

  9. spark基础了解—运行层次结构、standalone与onyarn

    spark程序运行层次结构 standalone即主从机制,后续添加了standaloneHA,zk管理master的存活,一旦master挂了会在候选master中诞生新的 HAstandalone ...

  10. 简单认识APP项目

    manifests:里面只有一个xml,是app运行配置文件 清单文件 <?xml version="1.0" encoding="utf-8"?> ...