当前位置: 首页 > news >正文

基于Redis实现的延迟队列

1. 适用场景

日常开发中,我们经常遇到这样的需求,在某个事件发生后,过一段时间做一个额外的动作,比如

  1. 拼单,如果2小时未能成单,取消拼单
  2. 下单,30分钟内未支付,取消订单
    之前的我们的做法通常是通过定时任务轮询,比如扫描创建时间是2小时之前,状态是未成功的拼单,然后做取消操作。这种方案存在的问题是:
  3. 扫描对数据库造成一定的压力
  4. 轮询的时间间隔会导致操作有一定的延迟
    延迟消息正是用来解决这类问题的银弹。

2. JDK实现

2.1 使用方式

JDK内部提供了DelayQueue队列和Delayed接口来实现延迟消息,我们先来看一个简单的Demo,我们会创建一个DelayMessage用来代表延迟消息,延迟消息需要实现Delayed接口

  1. getDealy,返回消息的延迟时间
  2. compareTo,为了让多个延迟消息排序,将时间最早的消息排到最前面
public class DelayMessage implements Delayed {private long expiredAtMs;private long delayMs;private String message;public DelayMessage(long delaySeconds, String message) {this.delayMs = delaySeconds * 1000;this.expiredAtMs = System.currentTimeMillis() + delayMs;this.message = message;}@Overridepublic long getDelay(TimeUnit unit) {long diff = expiredAtMs - System.currentTimeMillis();return unit.convert(diff, TimeUnit.MILLISECONDS);}@Overridepublic int compareTo(Delayed o) {long sTtl = getDelay(TimeUnit.MILLISECONDS);long oTtl = o.getDelay(TimeUnit.MILLISECONDS);return sTtl < oTtl ? -1 : (sTtl > oTtl ? 1 : 0);}public String getMessage() {return this.message;}
}

接着只需要创建消息队列,将延迟消息放入到队列中即可,然后创建一个线程来消费延迟队列即可

DelayQueue<DelayMessage> queue = new DelayQueue<>();
queue.put(new DelayMessage(1, "1s later"));
queue.put(new DelayMessage(60, "60s later"));
queue.put(new DelayMessage(120, "120s later"));ExecutorService es = Executors.newSingleThreadExecutor();
es.submit(() -> {try {while (true) {DelayMessage dm = queue.take();System.out.println(currentTimeInText() + "_" + dm.getMessage());}} catch (InterruptedException e) {throw new RuntimeException(e);}
});
2.2 实现原理

从DelayQueue的源码我们可以看到,整个DelayQueue的核心就在于3个点:

  1. 数据存储,基于PriorityQueue,通过Delayed的compareTo方法排序,即基于时间顺序
  2. 数据写入,offer/put方法
  3. 数据消费,take/poll方法
1. 数据写入
public boolean offer(E e) {final ReentrantLock lock = this.lock;lock.lock();try {q.offer(e);           // PriorityQueue写入if (q.peek() == e) {  // 如果刚刚写入的消息是最高优先级的(最早被消费的),唤醒在take()方法阻塞的线程leader = null;    // Leader-Follow Parttern,减少RaceCondition, http://www.cs.wustl.edu/~schmidt/POSA/POSA2/available.signal(); // 唤醒在take()阻塞的线程}return true;} finally {lock.unlock();}
}
2. 数据消费
public E take() throws InterruptedException {final ReentrantLock lock = this.lock;lock.lockInterruptibly();try {for (;;) {E first = q.peek();if (first == null)available.await();  // 队列为空,阻塞,直到offer(e)被调用else {long delay = first.getDelay(NANOSECONDS);if (delay <= 0)     // 延迟时间到了,取出item供使用return q.poll();first = null; // don't retain ref while waitingif (leader != null)available.await();  // await释放锁,其他线程执行take(),如果leader != null有负责处理头部item的线程else {Thread thisThread = Thread.currentThread();  // 走到这说明头部元素暂无处理线程,将当前线程设定为处理线程leader = thisThread;try {available.awaitNanos(delay);  // 等待延迟时间后自动唤醒,重新进入循环,处理queue头部item} finally {if (leader == thisThread)leader = null;}}}}} finally {if (leader == null && q.peek() != null)available.signal();lock.unlock();}
}

代码很短,设计还是巧妙的,尤其是Leader-Follower模式的使用,在我们实现自己的组件时可以借鉴。

3. Redis实现

JDK实现的延迟队列已经能解决部分场景了,不过也存在两个明显的问题

  1. 队列数据没持久化,重启或进程崩溃都会导致数据丢失
  2. 不支持分布式,不能跨进程共享
3.1 消息队列

通过上面的JDK实现,我们已经能把Redis实现的延迟消息的逻辑猜的八九不离十了,假设我们用LIST存储,先通过LPUSH写入队列消息(message1、message2)

127.0.0.1:6379> LPUSH my_delay_queue message1
(integer) 1
127.0.0.1:6379> LPUSH my_delay_queue message2
(integer) 2
127.0.0.1:6379> LRANGE my_delay_queue 0 -1
1) "message2"
2) "message1"

通过RPOPLPUSH,从队列取出待消费的消息,并暂存到临时队列(my_delay_queue)中

127.0.0.1:6379> RPOPLPUSH my_delay_queue my_delay_queue_temp
"message1"
127.0.0.1:6379> LRANGE my_delay_queue_temp 0 -1
1) "message1"
127.0.0.1:6379> LRANGE my_delay_queue 0 -1
1) "message2"

这是在程序代码中消费message1,如果消费成功,从临时队列中删除消息

127.0.0.1:6379> LREM my_delay_queue_temp 1 message1
(integer) 1

最终队列的状态是,delayQueue中只剩message2,临时队列中为空

127.0.0.1:6379> LRANGE my_delay_queue_temp 0 -1
(empty array)
127.0.0.1:6379> LRANGE my_delay_queue 0 -1
1) "message2"
3.2 延迟队列

用LIST只能实现FIFO,要想实现基于时间的优先级,需要改用ZSET来存储数据,用时间做时间戳

127.0.0.1:6379> ZADD s_delay_queue 1728625236 message0
127.0.0.1:6379> ZADD s_delay_queue 1728625256 message0
127.0.0.1:6379> ZADD s_delay_queue 1728625256 message2
127.0.0.1:6379> ZADD s_delay_queue 1728625266 message3127.0.0.1:6379> ZRANGE s_delay_queue 0 -1 WITHSCORES
1) "message0"
2) "1728625236"
3) "message1"
4) "1728625256"
5) "message2"
6) "1728625256"
7) "message3"
8) "1728625266"

通过使用ZRANGEBYSCORE获取延迟时间已经到的item

127.0.0.1:6379> ZRANGEBYSCORE s_delay_queue 0 1728625256
1) "message0"
2) "message1"
3) "message2"

ZSET并没有提供RPOPLPUSH的命令,我们使用Lua脚本来模拟这个操作,这段lua接受两个KEY,一个ARGV

  1. KEYS[1],表示ZSET的名字
  2. KEYS[2],表示LIST的名字
  3. ARGV[1],表示SCORE的范围截至时间
local elements = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1])
if #elements > 0 thenfor i, element in ipairs(elements) doredis.call('LPUSH', KEYS[2], element)redis.call('ZREM', KEYS[1], element)end
end
return elements

然后是通过EVAL执行这段Lua,这里我们从ZSET(s_delay_queue)读取score <= 1728625237的item,返回并暂存到LIST(s_delay_queue_temp)中,模拟了RPOPLPUSH的操作

127.0.0.1:6379> EVAL "local elements = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1]) if #elements > 0 then for i, element in ipairs(elements) do redis.call('LPUSH', KEYS[2], element) redis.call('ZREM', KEYS[1], element) end end return elements" 2 s_delay_queue s_delay_queue_temp 1728625237
1) "message0"

剩下的逻辑基本上和[[基于Redis的延迟队列#3.1 消息队列]]一样,在程序中消费message,成功之后删除s_delay_queue_temp中的数据。我们需要做的是在程序中定时的执行这段Lua脚本,并且实现类似DelayQueue的逻辑,支持阻塞的take()操作,以及消费失败时的错误处理,显然要处理的错误细节并不少。

3.3 Redisson实现
1. 数据结构

Redisson封装了基于Redis的延迟消息实现,我们来看一个使用的Redisson延迟队列的demo

Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);RBlockingQueue<String> blockingQueue = redisson.getBlockingQueue("delayBlockingQueue");
RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue(blockingQueue);delayedQueue.offer("message1", 1, TimeUnit.MINUTES);
delayedQueue.offer("message2", 5, TimeUnit.MINUTES);
delayedQueue.offer("message3", 10, TimeUnit.MINUTES);
delayedQueue.offer("message4", 15, TimeUnit.MINUTES);ExecutorService es = Executors.newSingleThreadExecutor();
es.submit(() -> {while (true) {String data = blockingQueue.poll(60, TimeUnit.SECONDS);if (data != null) {System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + ":" + data);}}
});

Redisson的实现比[[#3.2 延迟队列]]要负责一点,它内部构建了4个数据结构。通过Redis的命令查看,我们能看到3个KEY

127.0.0.1:6379> KEYS *
2) "delayBlockingQueue"
4) "redisson_delay_queue:{delayBlockingQueue}"
6) "redisson_delay_queue_timeout:{delayBlockingQueue}"
  1. delayBlockingQueue是我们创建RBlockingQueue时指定的名称,用来存储延迟时间到期,但尚未被处理的任务
  2. redisson_delay_queue_timeout:{delayBlockingQueue},类型是zset,记录延迟任务和时间
  3. redisson_delay_queue:{delayBlockingQueue},类型是list,记录任务列表,保持任务的顺序
    通过TYPE命令,我们能查看他们的数据类型
127.0.0.1:6379> TYPE redisson_delay_queue:{delayBlockingQueue}
list
127.0.0.1:6379> TYPE redisson_delay_queue_timeout:{delayBlockingQueue}
zset

此外Redission还创建了一个Channel,用来在delayQueue写入数据的时候做通知

127.0.0.1:6379> PUBSUB channels
1) "redisson_delay_queue_channel:{delayBlockingQueue}"
2. 数据写入

通过RDelayedQueue写入数据的时候,最终会调用offerAsync方法

public RFuture<Void> offerAsync(V e, long delay, TimeUnit timeUnit) {if (delay < 0) {throw new IllegalArgumentException("Delay can't be negative");}long delayInMs = timeUnit.toMillis(delay);long timeout = System.currentTimeMillis() + delayInMs;long randomId = ThreadLocalRandom.current().nextLong();return commandExecutor.evalWriteAsync(getName(), codec, RedisCommands.EVAL_VOID,"local value = struct.pack('dLc0', tonumber(ARGV[2]), string.len(ARGV[3]), ARGV[3]);" + "redis.call('zadd', KEYS[2], ARGV[1], value);"                       // 写入  redisson_delay_queue_timeout:{delayBlockingQueue}+ "redis.call('rpush', KEYS[3], value);"                               // 写入  redisson_delay_queue:{delayBlockingQueue}// if new object added to queue head when publish its startTime // to all scheduler workers + "local v = redis.call('zrange', KEYS[2], 0, 0); "                    // 取时间戳最小的元素+ "if v[1] == value then "+ "redis.call('publish', KEYS[4], ARGV[1]); "                       // 如果新插入的元素是zset的第一个元素,做channel通知+ "end;",Arrays.<Object>asList(getName(), timeoutSetName, queueName, channelName), timeout, randomId, encode(e));
}
3. 数据消费

创建RDelayedQueue时,redisson创建了一个QueueTransferTask任务,负责从redisson_delay_queue_timeout:{delayBlockingQueue}将到期的数据迁移到delayBlockingQueue

protected RedissonDelayedQueue(QueueTransferService queueTransferService, Codec codec, final CommandAsyncExecutor commandExecutor, String name) {super(codec, commandExecutor, name);channelName = prefixName("redisson_delay_queue_channel", getName());queueName = prefixName("redisson_delay_queue", getName());timeoutSetName = prefixName("redisson_delay_queue_timeout", getName());QueueTransferTask task = new QueueTransferTask(commandExecutor.getConnectionManager()) {@Overrideprotected RFuture<Long> pushTaskAsync() {return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG,"local expiredValues = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); " // 从redisson_delay_queue_timeout拿到期的任务+ "if #expiredValues > 0 then "+ "for i, v in ipairs(expiredValues) do "+ "local randomId, value = struct.unpack('dLc0', v);"+ "redis.call('rpush', KEYS[1], value);"      // 写入到 delayBlockingQueue+ "redis.call('lrem', KEYS[3], 1, v);"        // 从 redisson_delay_queue 删除+ "end; "+ "redis.call('zrem', KEYS[2], unpack(expiredValues));" // 从 redisson_delay_queue_timeout 删除+ "end; "// get startTime from scheduler queue head task+ "local v = redis.call('zrange', KEYS[2], 0, 0, 'WITHSCORES'); "+ "if v[1] ~= nil then " // 如果最小时间戳的任务存在,返回它的时间戳+ "return v[2]; "+ "end "+ "return nil;",Arrays.<Object>asList(getName(), timeoutSetName, queueName),  // KEYS: delayBlockingQueue , redisson_delay_queue_timeout*、redisson_delay_queue*System.currentTimeMillis(), 100);}@Overrideprotected RTopic getTopic() {return new RedissonTopic(LongCodec.INSTANCE, commandExecutor, channelName);}};queueTransferService.schedule(queueName, task);this.queueTransferService = queueTransferService;
}
4. RBlockingQueue

通过[[#3. 数据消费]]的操作,redisson已经将到期的延迟任务写入到delayBlockingQueue了,剩下要做的就是用delayBlockingQueue实现阻塞队列了,核心代码在 RedissonBlockingQueue,其实实现很简单,我们来看下代码,take()方法实际只是执行了一个redis命令BLPOP

@Override
public V take() throws InterruptedException {return commandExecutor.getInterrupted(takeAsync());
}
@Override
public RFuture<V> takeAsync() {return commandExecutor.writeAsync(getName(), codec, RedisCommands.BLPOP_VALUE, getName(), 0);
}

相关文章:

基于Redis实现的延迟队列

1. 适用场景 日常开发中&#xff0c;我们经常遇到这样的需求&#xff0c;在某个事件发生后&#xff0c;过一段时间做一个额外的动作&#xff0c;比如 拼单&#xff0c;如果2小时未能成单&#xff0c;取消拼单下单&#xff0c;30分钟内未支付&#xff0c;取消订单 之前的我们的…...

LINUX——内核移植、内核编译教程

Linux内核编译是一个将内核源代码转换成可在特定硬件架构上运行的二进制文件的过程。以下是编译Linux内核的一般步骤&#xff1a; 1、准备工作&#xff1a; 确保安装了必要的编译工具&#xff0c;如gcc、make、ncurses库&#xff08;用于make menuconfig&#xff09;等。 2、…...

《OpenCV计算机视觉》—— 用于执行图像透视变换的两个关键函数

文章目录 cv2.getPerspectiveTransformcv2.warpPerspective注意事项 cv2.getPerspectiveTransform 和 cv2.warpPerspective 是 OpenCV 库中用于执行透视变换的两个关键函数。下面是对这两个函数的详细解释&#xff1a; cv2.getPerspectiveTransform 功能&#xff1a;计算从源…...

uniapp使用字体图标 ttf svg作为选项图标,还支持变色变图按

在staic目录下有一些ttf文件&#xff0c;如uni.ttf&#xff0c;iconfont.ttf 这些文件中保存这字体svg的源码们&#xff0c;我们也可以在网上找其他的。这些就是我们要显示的突图标的 显示来源。这样不用使用png图标&#xff0c;选中不选中还得用两个图片 我的具体使用如下 &q…...

<Project-6 pdf2tx> Python Flask 应用:图片PDF图书的中文翻译解决方案

重要更新&#xff01; Modified on 8oct24. P6已经被 P8 替代&#xff0c;后着支持多任务&#xff0c;多翻译机。在速度与资源占用上&#xff0c;都好于这个P6。 新的 P8 文章链接&#xff1a; &#xff1c;Project-8 pdf2tx-MM&#xff1e; Python Flask应用&#xff1a;在…...

10.11Python数学基础-多维随机变量及其分布

多维随机变量及其分布 1.二维随机变量及其分布 假设E是随机试验&#xff0c;Ω是样本空间&#xff0c;X、Y是Ω的两个变量&#xff1b;(X,Y)就叫做二维随机变量或二维随机向量。X、Y来自同一个样本空间。 联合分布函数 F ( x , y ) P ( X ≤ x , Y ≤ y ) F(x,y)P(X≤x,Y≤…...

(四)Mysql 数据库备份恢复全攻略

一、数据库备份 数据库备份目的和数据库故障类型 目的&#xff1a; 当发生故障时&#xff0c;将损失降到最低。保证能够快速从备份数据中恢复&#xff0c;确保数据稳定运行。故障类型&#xff1a; 程序错误&#xff1a;Mysql 服务器端程序故障无法使用。人为误操作&#xff1a;…...

在MySQL 8.0中,如何更好地管理索引以节省空间和提高查询效率?

1. 索引选择与设计 选择合适的列&#xff1a;确保索引覆盖的列是经常用于查询条件、排序或连接操作的列。避免冗余索引&#xff1a;检查并移除重复或不必要的索引。例如&#xff0c;如果已经有一个 INDEX(a, b)&#xff0c;那么单独的 INDEX(a) 可能是多余的。使用复合索引&am…...

图形化编程(013)——“面向鼠标指针”积木块

知识回顾 1、舞台和坐标的知识 2、使用坐标控制角色移动 一句俗语&#xff1a;大鱼吃小鱼&#xff0c;小鱼吃虾米&#xff0c;感觉挺有意思的。 这句话说明了自然界中的生存法则&#xff0c;本次分享我与大家共同做一个大鱼吃小鱼的作品。 案例解说&#xff1a; 点击绿旗…...

【Spring】Spring Boot项目创建和目录介绍

文章目录 1 Spring Boot 介绍2 Spring Boot 项目创建注意事项 3. 项目代码和目录介绍pom 文件父工程目录介绍 1 Spring Boot 介绍 Spring 让 Java 程序更加快速、简单和安全&#xff0c;Spring 对于速度、简单性和生产力的关注使其成为世界上最流行的 Java 框架 Spring 官方提…...

第十二章 RabbitMQ之失败消息处理策略

目录 一、引言 二、RepublishMessageRecoverer 实现 2.1. 实现步骤 2.2. 实现代码 2.2.1. 异常交换机队列回收期配置类 2.2.2. 常规交换机队列配置类 2.2.3. 消费者代码 2.2.4. 消费者yml配置 2.2.5. 生产者代码 2.2.6. 生产者yml配置 2.2.7. 运行效果 一、引言 …...

23年408数据结构

第一题&#xff1a; 解析&#xff1a; 第一点&#xff0c;我们要知道顺序存储的特点&#xff1a;优点就是随用随取&#xff0c;就是你想要查询第几个元素可以直接查询出来&#xff0c;时间复杂度就是O(1)&#xff0c;缺点就是不适合删除和插入&#xff0c;因为每次删除和插入一…...

vue3ElementPlu表格合并多行

// 单元格合并逻辑 const objectSpanMethod ({ row, rowIndex, columnIndex }) > { const previousMachineModelUniqueId rowIndex > 0 ? tableData.value[rowIndex - 1].machineModel : null; const currentMachineModelUniqueId row.machineModel; // 合并“机型”…...

MySQL数据库 - 索引(上)

目录 1 简介 1.1 索引是什么 1.2 为什么要使用索引 2 索引应该选择哪种数据结构 2.1 HASH 2.2 二叉搜索树 2.3 N叉树&#xff08;B树&#xff09; 2.4 B树 3 MySQL的页 3.1 为什么要使用页 3.2 页文件头和页文件尾 3.3 页主体 3.4 页目录 4 B树在MySQL索引中的应…...

redis与springBoot整合

前提 要实现,使用Redis存储登录状态 需要一个完整的前端后端的项目 前端项目搭建 解压脚手架 安装依赖 配置请求代理 选做: 禁用EsLint语法检查 Vue Admin Template关闭eslint校验&#xff0c;lintOnSave&#xff1a;false设置无效解决办法_lintonsave: false-CSDN博客 …...

YoloV9改进策略:BackBone改进|CAFormer在YoloV9中的创新应用,显著提升目标检测性能

摘要 在目标检测领域,模型性能的提升一直是研究者和开发者们关注的重点。近期,我们尝试将CAFormer模块引入YoloV9模型中,以替换其原有的主干网络,这一创新性的改进带来了显著的性能提升。 CAFormer,作为MetaFormer框架下的一个变体,结合了深度可分离卷积和普通自注意力…...

消防应急物资仓库管理系统

集驰电子消防装备仓库管理系统(DW-S302系统)是一套成熟系统&#xff0c;依托3D技术、大数据、RFID技术、数据库技术、对装备器材进行统一管理&#xff0c;以RFID射频识别技术为核心&#xff0c;构建以物资综合管理为基础&#xff0c;智能分析定位为主要特色功能的装备器材库综合…...

【论文阅读】Semi-Supervised Few-shot Learning via Multi-Factor Clustering

通过多因素聚类的半监督小样本学习 引用&#xff1a;Ling J, Liao L, Yang M, et al. Semi-supervised few-shot learning via multi-factor clustering[C]//Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022: 14564-14573. 论文地址…...

第十三章 RabbitMQ之消息幂等性

目录 一、引言 二、消息幂等解决方案 2.1. 方案一 2.2. 方案二 一、引言 幂等是一个数学概念&#xff0c;用函数表达式来描述是这样的&#xff1a;f(x) f(f(x)) 。在程序开发中&#xff0c;则是指同一个业务&#xff0c;执行一次或多次对业务状态的影响是一致的。有些业务…...

tpcms-master.zip

网盘&#xff1a;https://pan.notestore.cn/s.html?id34https://pan.notestore.cn/s.html?id34...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻

在如今就业市场竞争日益激烈的背景下&#xff0c;越来越多的求职者将目光投向了日本及中日双语岗位。但是&#xff0c;一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧&#xff1f;面对生疏的日语交流环境&#xff0c;即便提前恶补了…...

Spring Boot 实现流式响应(兼容 2.7.x)

在实际开发中&#xff0c;我们可能会遇到一些流式数据处理的场景&#xff0c;比如接收来自上游接口的 Server-Sent Events&#xff08;SSE&#xff09; 或 流式 JSON 内容&#xff0c;并将其原样中转给前端页面或客户端。这种情况下&#xff0c;传统的 RestTemplate 缓存机制会…...

循环冗余码校验CRC码 算法步骤+详细实例计算

通信过程&#xff1a;&#xff08;白话解释&#xff09; 我们将原始待发送的消息称为 M M M&#xff0c;依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)&#xff08;意思就是 G &#xff08; x ) G&#xff08;x) G&#xff08;x) 是已知的&#xff09;&#xff0…...

基于Flask实现的医疗保险欺诈识别监测模型

基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施&#xff0c;由雇主和个人按一定比例缴纳保险费&#xff0c;建立社会医疗保险基金&#xff0c;支付雇员医疗费用的一种医疗保险制度&#xff0c; 它是促进社会文明和进步的…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)

服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...

智能在线客服平台:数字化时代企业连接用户的 AI 中枢

随着互联网技术的飞速发展&#xff0c;消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁&#xff0c;不仅优化了客户体验&#xff0c;还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用&#xff0c;并…...

Caliper 配置文件解析:config.yaml

Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

2025季度云服务器排行榜

在全球云服务器市场&#xff0c;各厂商的排名和地位并非一成不变&#xff0c;而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势&#xff0c;对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析&#xff1a; 一、全球“三巨头”…...

Go 并发编程基础:通道(Channel)的使用

在 Go 中&#xff0c;Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式&#xff0c;用于在多个 Goroutine 之间传递数据&#xff0c;从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...