图解分布式定时器从零实现 | go语言(一)

参考

https://zhuanlan.zhihu.com/p/600380258
https://xie.infoq.cn/article/aaa353c9df6641eb1b09e6f36
https://www.luozhiyun.com/archives/458


前言

在许多业务场景中,我们需要使用定时器来执行一些定期任务或操作。以下是一些常见的使用场景:

  1. 订单管理

    • 当订单一直处于未支付状态时,需要及时关闭订单并退还库存
    • 对于处于退款状态的订单,需要定期检查是否已经退款成功
  2. 用户激活

    • 对于新创建的店铺,如果在N天内没有上传商品,系统需要发送激活短信提醒
  3. 数据统计

    • 需要定期统计并更新各种业务指标和报表数据
  4. 缓存刷新

    • 需要定期刷新或更新缓存数据,以确保数据的及时性和准确性
  5. 消息队列消费

    • 需要定期从消息队列中消费并处理消息
  6. 系统维护

    • 需要定期执行一些系统维护任务,如日志清理、数据备份等

这些场景中,如果使用传统的扫表方式,每个业务都需要维护自己的扫表逻辑,会导致大量重复代码。使用定时器,可以将这部分逻辑抽象出来,作为一个公共的组件,提高代码的可维护性和复用性。定时器可以根据配置的时间间隔,周期性地执行指定的任务,从而解决上述问题。


核心思路

主动轮询

一句话总结:实现定时器最简单粗暴的方式:轮询 + 触发.

(1)注册定时器:解析并将一系列定时任务平铺直叙地展开,每笔定时任务明确展示执行时间这一指标

(2)节点自轮询:每间隔一个微小的时间范围,对定时任务列表进行全量查询

(3)过滤&触发:以 执行时间小于等于当前时刻 作为过滤条件,摘出满足执行条件的定时任务进行执行.

这个乞丐版定时器虽然已经实现了,但是它还有很多可以优化的地方,比如每次查询都需要承担 O(N) 的线性时间复杂度。显然,我们还有很多地方可以进一步改进。
在这里插入图片描述

存储结构优化:有序表

一句话总结:基于有序表提升查询效率

针对无序表结构,虽然插入记录可以达到 O(1) 的时间复杂度,但每次查询都需要 O(N) 的线性时间复杂度,这显然是一个短板。解决这个问题的一个优化方向是通过重新设计存储结构,将时间复杂度均摊到每一笔操作当中。

例如使用红黑树(RBTree) 或者跳表(Skip List) 这样的数据结构,能以将插入记录的时间复杂度由 O(1) ->O(logN)为代价,换取查询时间复杂度O(N) -> O(logN) 的优化.

在 xTimer 的实现中,存储介质选型上选择使用 Redis ZSet,以定时任务执行时间Score 进行有序结构的搭建,当定时任务数量达到一定量级时,ZSet 底层基于跳表作为有序表的实现. 一些更细致的实现流程如下:

(1)以 Redis ZSet 作为存储介质;

(2)每次添加定时任务时,执行 ZAdd 动作,以执行时间的时间戳作为排序的键(Score) 进行有序结构的搭建;

(3)每次查询定时任务时,执行 ZRangeByScore 动作,以当前时刻的时间戳加上一个微小偏移量作 score 的左右边界;

经过优化,将木桶的短板从 O(N) 提升到 O(logN),这是时间复杂度模型的优化。接下来,可以通过数据分治的方式对查询任务数量 N 进行优化;
在这里插入图片描述
下面给出了简单的代码


package main

import (
	"fmt"
	"github.com/go-redis/redis/v8"
	"context"
	"time"
)

var ctx = context.Background()

type RedisTimer struct {
	client *redis.Client
}

func NewRedisTimer() *RedisTimer {
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // no password set
		DB:       0,  // use default DB
	})
	return &RedisTimer{client}
}

func (r *RedisTimer) AddTask(taskID string, executionTime int64) error {
	_, err := r.client.ZAdd(ctx, "tasks", &redis.Z{Score: float64(executionTime), Member: taskID}).Result()
	return err
}

func (r *RedisTimer) QueryTasks() ([]string, error) {
	currentTime := time.Now().Unix()
	minScore := fmt.Sprintf("%d", currentTime - 1) // 设置一个微小的偏移量,确保查询到当前时间之后的任务
	maxScore := "+inf"
	result, err := r.client.ZRangeByScore(ctx, "tasks", &redis.ZRangeBy{
		Min: minScore,
		Max: maxScore,
	}).Result()
	if err != nil {
		return nil, err
	}
	return result, nil
}

func main() {
	redisTimer := NewRedisTimer()

	// 添加定时任务
	err := redisTimer.AddTask("task1", time.Now().Unix() + 10)  // 10秒后执行
	if err != nil {
		fmt.Println("Error adding task1:", err)
	}
	err = redisTimer.AddTask("task2", time.Now().Unix() + 20)  // 20秒后执行
	if err != nil {
		fmt.Println("Error adding task2:", err)
	}

	// 查询定时任务
	tasks, err := redisTimer.QueryTasks()
	if err != nil {
		fmt.Println("Error querying tasks:", err)
	} else {
		fmt.Println("当前定时任务:", tasks)
	}
}


存储结构优化:纵向分治

一句话总结:通过时间范围分片,减少查询涉及的任务数量

每次我们进行查询时,我们最关心的是即将要执行的任务,也就是那些在接下来的一段时间内即将发生的。但是,数据中可能会包含一些计划在稍后执行的任务,它们在当前的查询中并不是我们的主要焦点,只是在增加数据量而已。为**了更清楚地处理这个问题,我们把整个时间分成了许多小片段,就像把一根线剪成了很多小段一样。这样,我们可以更容易地识别出当前时间段内需要关注的任务,而那些在未来时间段才会执行的任务则暂时不被考虑。**这种方法让我们更好地管理任务,避免了不必要的数据干扰,确保我们能够专注于当下最重要的事情。

xTimer 的实现中,选用 1 分钟作为分片的时间范围,更细致的实现流程如下:

(1)插入每笔定时任务时,根据执行时间推算出所属的分钟级时间范围表达式,例如:2022-09-17-11:00:03 -> 2022-09-17-11:00:00_2022-09-17-11:01:00

(2)以分钟级时间范围表达式为 key,将定时任务任务插入到不同 ZSet 中,组成一系列相互隔离的有序表结构。

(3)每一次查询过程中,同样根据当前时刻推算出对应分钟级时间范围表达式,并以此为 key 查找到对应的有序表进行 ZRange 查询。

至此,每一次查询的任务量级就从全量数据 N 进一步减小到分钟级数据 N',毋庸置疑 N' << N

在这里插入图片描述


存储结构优化:横向分治

一句话总结:通过定时任务分桶,提高并发度.

前面的小节,我们都基于单核的视角出发,对查询和存储模型进行优化。然而我们的主题是生产环境中 golang 分布式定时器的实现方案,因此必然是集群模式,且单个节点也可以基于 goroutine 实现高并发

为了避免引起多协程介入导致对临界资源的竞态问题,xTimer 在实现上以分片作为最小的资源粒度,每一个分片对应的任务集只会由一个goroutine负责作轮询,因此相应的要求是,需要将时间分片拆解为更细的粒度,即在横向上额外增加一个分桶的维度,从而保证每个时间范围内能有对应于分桶数量的goroutine并发参加工作。

更细致的流程如下:

(1)插入定时任务时,首先根据执行时间,确定其从属的时间范围;

(2)其次,根据定时任务的唯一标识 id,结合服务对最大桶数的设置参数,随机将定时任务划分到一个桶中;

(3)以时间范围和桶号组装形成一个新的 key,形成一个二维分片,实现对定时任务有序表的隔离;

(4)后续流程与 横向分治小节相同.

至此,分布式定时器核心方案的轮廓已经呈现,本质上是一个主动轮询的模型,过程中辅以有序表结合分时分桶的方式进行轮询效率的优化。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/504585.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构——lesson12排序之归并排序

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

C++2D原创我的世界1.00.3版本上市!!!

我很郁闷&#xff0c;为什么就是整不了昼夜交替啊喂&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 虽然这看上去很简单&#xff0c;但做起来要我命&#xff01;&#xff01;&#xff01; 优化过后总共1312行&#xff0c…

Linux:内核源代码角度看文件和Socket

文章目录 文件和Socket 文件和Socket 在之前写的网络服务&#xff0c;它们的本质其实就是一个进程&#xff0c;而对于每一个打开的文件来说&#xff0c;都要有一个自己对应的文件描述符&#xff0c;其中会默认打开对应的012&#xff0c;作为标准输入标准输出标准错误&#xff…

数据结构——lesson13排序之计数排序

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

如何简化多个 if 的判断结构

多少算太多&#xff1f; 有些人认为数字就是一&#xff0c;你应该总是用至少一个三元运算符来代替任何单个 if 语句。我并不这样认为&#xff0c;但我想强调一些摆脱常见的 if/else 意大利面条代码的方法。 我相信很多开发人员很容易陷入 if/else 陷阱&#xff0c;不是因为其…

ThreadLocal的基本使用

一、ThreadLocal的介绍 ThreadLocal 是 Java 中的一个类&#xff0c;它提供了线程局部变量的功能。线程局部变量是指每个线程拥有自己独立的变量副本&#xff0c;这些变量在不同的线程中互不影响。ThreadLocal 提供了一种在多线程环境下&#xff0c;每个线程都可以独立访问自己…

PS从入门到精通视频各类教程整理全集,包含素材、作业等(4)

PS从入门到精通视频各类教程整理全集&#xff0c;包含素材、作业等 最新PS以及插件合集&#xff0c;可在我以往文章中找到 由于阿里云盘有分享次受限制和文件大小限制&#xff0c;今天先分享到这里&#xff0c;后续持续更新 PS人物数码照片处理技法视频教程 https://www.…

武汉星起航:一站式跨境电商服务引领者,专业高效助力客户出海

武汉星起航电子商务有限公司&#xff0c;坐落于华中地区的商业核心地带——湖北武汉&#xff0c;自公司成立以来&#xff0c;便以提供一站式跨境电商服务为核心发展&#xff0c;致力于为广大客户提供专业、高效、全面的出海解决方案。凭借5对1服务体系、ERP软件授权、中转仓服务…

二、分布式事务

目录 二、分布式事务2.1 什么是分布式事务2.2 分布式事务产生的背景2.3 分布式事务产生的场景2.4 分布式事务理论4.1 CAP理论4.2 Base理论 5、分布式事务的解决方案 二、分布式事务 2.1 什么是分布式事务 一组操作会产⽣多个数据库session会话 此时就会出现分布式事务 2.2 分…

游戏软件出现d3dcompiler_47.dll缺失怎么修复,亲测的六种有效方法推荐

D3DCompiler47.dll是DirectX SDK中的一个重要组件&#xff0c;它提供了将HLSL&#xff08;High-Level Shading Language&#xff09;着色器编译为可执行代码的功能。通过使用D3DCompiler47.dll&#xff0c;开发人员可以将复杂的着色器代码转换为可以在GPU上高效运行的机器代码&…

黑马点评项目笔记 II

基于Stream的消息队列 stream是一种数据类型&#xff0c;可以实现一个功能非常完善的消息队列 key&#xff1a;队列名称 nomkstream&#xff1a;如果队列不存在是否自动创建&#xff0c;默认创建 maxlen/minid&#xff1a;设置消息队列的最大消息数量 *|ID 唯一id&#xff1a;…

Vue系列-el挂载

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>el:挂载点</title> </head> <body&g…

作业 二维数组-定位问题

图形相似度 描述 给出两幅相同大小的黑白图像&#xff08;用0-1矩阵&#xff09;表示&#xff0c;求它们的相似度。 说明&#xff1a;若两幅图像在相同位置上的像素点颜色相同&#xff0c;则称它们在该位置具有相同的像素点。 两幅图像的相似度定义为相同像素点数占总像素点数…

P87 4.1 C++ FOR 与Delphi FOR 的区别

输出x, sin(x), cos(x), tan(x)的值。已知X0&#xff0c;10&#xff0c; 20&#xff0c;180。 我用Delphi编写了程序&#xff1a; 第10行出现 给FOR 循环变量赋值i错误。 C中是可以的&#xff0c; 详见&#xff1a;delphi循环的一个小知识_assignment to for-loop variable…

安装JupyterLab的集成环境

Python集成环境安装 不要半途而废&#xff0c;不要作业太多就抛下你手中的笔&#xff0c;拿起你旁边的手机&#xff0c;你觉得这样很有意义吗&#xff1f;一个小时一道题都没做&#xff0c;盯着手机屏幕它能给你一个未来吗&#xff1f;少分心就能多做一道题&#xff0c;多学样本…

Java多线程:定位死锁

检测死锁可以使用jconsole工具&#xff0c;或使用jps定位进程id&#xff0c;再用jstack定位死锁 方案1&#xff1a; 1. 先用jps查看所有的java进程id 2. jstack 进程id定位死锁 3. 查看死锁结果 方案2:从jdk的安装路径中找到bin目录, 点击jconsole

Kafka入门到实战-第五弹

Kafka入门到实战 Kafka常见操作官网地址Kafka概述Kafka的基础操作更新计划 Kafka常见操作 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://kafka.apache.org/Kafka概述 Apache Kafka 是一个开源的分布式事件流平台&…

1.5编写一个程序,输入梯形的上底,下底和高,输出梯形的面积。

1、编写一个程序,输入梯形的上底,下底和高,输出梯形的面积。 package com.kangning.web.controller.system;import java.util.Scanner;/*** 编写一个程序,输入梯形的上底,下底和高,输出梯形的面积。*/ public class CountArea {public static void main(String[] args) …

知乎:多云架构下大模型训练,如何保障存储稳定性?

知乎&#xff0c;中文互联网领域领先的问答社区和原创内容平台&#xff0c;2011 年 1 月正式上线&#xff0c;月活跃用户超过 1 亿。平台的搜索和推荐服务得益于先进的 AI 算法&#xff0c;数百名算法工程师基于数据平台和机器学习平台进行海量数据处理和算法训练任务。 为了提…

java入门学习Day01

本篇文章主要是学会如何使用IDEA&#xff0c;和运行第一个java文件。 java环境安装&#xff1a;Windows下Java环境配置教程_windows java环境配置-CSDN博客 IDEA安装&#xff1a;IDEA 2023.2.5 最新激活码,注册码&#xff08;亲测好用&#xff09; - 异常教程 以上两个链接…