我们在开发互联网产品的时候,经常会遇到这样的业务场景,例如:

我们在电商网站下了一个订单,电商平台可能要求我们一定时间内完成支付,否则订单就会被自动取消;

我们在工作协同平台上预约了一个会议,在会议即将开始前15分钟,协同平台会自动给与会者发一个通知提醒;

我们为经纪人提供的产品中,经纪人预约了某个时间前往看房,到时候系统会提前发通知提醒经纪人;

在互联网产品中,类似的需要在未来的某个不长的时间后做某事的场景非常多,这个功能看起来很简单,但并不见得广大工程师们都能实现的好。

在面试工程师的过程中,我时常会基于这种场景问一些类似的问题,看看候选人用计算机工程方法解决实际问题的思路。遗憾的是,大多数候选人很快给出的答案是:这个很简单,我写一个定时任务,定时执行一段代码,这段代码会去查询数据库,看看有没有超时需要取消的订单、有没有即将到时需要提醒的会议、有没有需要提醒的看房预约等。并且讲了很多在 Java 或其他语言体系中比较成熟的定时任务实现工具,例如经常会被提到的 Quartz、Spring-Task 等,有的甚至用 Linux 的 crontab 实现定时驱动。

遇到这样的回答,我往往会进一步提出下列几个问题:

如果现实中这样的请求量很大怎么办?比如你想象一下淘宝这样的电商平台每秒钟产生的订单量;

如果实际场景要求的时间精度非常高怎么办?比如要求精确到秒甚至更高。

这时候就会意识到上述粗暴的解决方案行不通,因为:

当数据量很大的时候,相应的业务数据库(例如上面第一个例子中的订单库)中需要被扫描检查的数据特别多,这势必需要定时地去访问大量的数据,从中找到寥寥无几的中标者。这是一种极大的浪费和低效,弄不好会把数据库给拖跨,导致整个系统性能低下;

精度要求很高的时候,上述方法就更行不通了,那个定时任务必须高频运行,这势必雪上加霜,甚至因为每次检查大量数据本身就需要很长时间,根本就不能实现想要的精度;

而且程序中存在大量的定时任务真的是一件很糟糕的事情,因为你不知道某个时间程序会批量干了什么事情,影响大量的数据,如果出现故障将很难排查。

在计算机工程思想中,有一条原则就是,要让计算机尽量少做工作,特别是少做无用的工作。这样才可以在有限的计算机资源情况下做更多的事情,节省成本,获得更好的性能体验,进而为用户提供更好的产品体验。甚至在数据量高速增加的情况下,性能也不会有明显的降低,以及必要时可以通过简单的 Scale-Out 扩容。我们所熟知的各种基础算法和数据结构,如二分查找法、HashTable、B+Tree 等都是通过指数降低时间复杂度而获得很好的时间成本优势进而解决了很多实际问题的。

数据结构和算法存在的重要目的之一就是让我们通过事先组织或摆放好数据,并通过某种巧妙的计算方式,用较小的成本去应对大量的数据问题的。

说到这里,我回忆起很多年前刚刚毕业踏入软件工程师这个职业时,我最尊敬的启蒙导师给我的启发。当时我们做一个大型的移动通讯网络交换系统,在电话呼叫的时候,有很多这样的定时器场景,例如当对方振铃120秒后需要自动挂断;挂断电话的时候如果对方没有回应,就可能导致稀缺的有线或无线资源被一直占用而耗尽,所以设备间的连接需要定时 heartbeat。这就是通信网络中各种协议(protocol)必须要考虑的场景,全球互通的电信网络就是这样工作的,我们可以想象成类似互联网 TCP 协议中的建立连接和关闭连接这样的场景。因为系统中有非常多的模块,且一个市或省的电信局每天可能有几千万个呼叫,我们不希望每个模块的工程师都单独去实现定时逻辑,而且因为这些程序是在特定的电路板上工作的,计算资源非常有限,且时间准确度要求非常苛刻(我记得当时我们团队的名字就是 Real-Time Team)。我的导师将这个任务交给了我,当时我也是一时找不到好方法实现,他就说:你仔细观察一下你家的闹钟是怎么工作的。于是我跑到某个会议室拿来一个下图这种转盘式的钟表盯着看了一下午,于是茅塞顿开。

我们订闹钟的时候,其实是告诉闹钟,你在未来的几点几分几秒叫醒我。这个闹钟就像一个服务器程序,我们给他一个定时指令,他在未来给我们个通知。我们的各种客户端可以给他成千上万的定时指令,他这个服务要做的就是记住这些指令,并在每个对应的时间点给当初下指令的那个客户端发个通知,就像下图这样:

于是我们就可以设计这样一个数据结构,用一个环形的数组来模拟闹钟的转盘刻度(所谓环形数组就是当我们顺序移动数组下标到最后一个槽位时,再从第0个槽位开始继续移动),假设这个闹钟的时间精度是1秒且这个转盘转一圈正好是一天,每个数组槽位代表一天中的某个确定的秒数,那么这个数组的长度就是246060=86400。在每个数组槽位上我们挂一个链表,这个链表就是为了存放客户端注册的闹钟请求的。然后我们可以提供服务接口,当有人请求在未来某个时间需要提醒的时候,我们就将这个请求的信息记录在对应的时间槽位上的链表末尾,即在这个节点上记录下是谁要求在未来时钟走到这里时提醒他,当然请求者可能要求几天后(也就是超过了这个环的长度),那我们还要记录下时钟要转的圈数。

然后我们在这个服务中启动一个线程模拟秒针,每秒滴答运行一次,检查对应槽位的链表上是否有挂着节点,如果有并且圈数参数已经减到了0,那就给当初的请求者发一条提醒消息(比如可以用 Message Queue 发送)告诉他时间到了,你该干嘛干嘛吧。如果记录圈数的参数尚未减到0,就将他减1,等下次遇到他再说。这样一个基本的闹钟服务就设计好了。

这种经过一番设计的闹钟服务的好处是:

每次只检查少量的数据,数据访问和计算开销都非常小,成本极低;

精度可以做到很小,比如秒;

整个产品体系中各个业务模块需要闹钟提醒的时候,只需要调用API注册一个闹钟请求即可,剩下的就是等通知了,让业务开发非常简单敏捷;

业务系统每次离散地收到通知只需处理特定的数据,对业务系统不会有冲击,不会担心随着数据量变大而影响业务系统本身的性能。

你看,这种设计和现实中的闹钟是不是非常相似,有时候计算机世界的解决方案就是对现实世界的模仿,类似的例子有很多。而且这个解决方案中的数据模型和我们常见的 HashTable 非常相似,都是将大量数据分散到多个小的数据集中,让每次计算少做无用功。

当然,从工程上讲,上面只是描述了基本的设计原理,要让他成为一个真正可以稳定工作并可以平滑扩展的服务还有下列工作要做:

我们不能把这个闹钟的环只放在内存中,那样当服务器重启后之前所注册的闹钟请求都消失了,因此我们可以考虑将数据放在 Redis 这样的带有持久化功能的内存数据库中,当服务重启后要从之前中断的点做回放;

实际场景中,有些闹钟请求时间很长,比如好几天后,显然这样的数据都放在内存或 Redis 中比较浪费,我们可以把时间久远的数据当做“冷”数据暂时存放在 MySQL 这样的数据库中,并平滑地将近期可能使用的“热”数据加载进入环中;

有些请求并不需要那么高的精度,比如分钟就可以了,那么我们就可以实现多个不同精度的环;

当业务量持续扩大,一个服务器已经吃不消了,或者我们环上的链表太长了,我们就可以部署多台服务器,实现水平 Scale-Out 扩展。

你看,这么个看似简单的功能,实现起来要有一些思考和设计,巧妙地利用数据结构和简单的算法优雅、低成本地实现;工程上还要考虑稳定性,可扩展性等多个方面才能成为一个可用的服务。

因为进行了抽象设计并和特定的业务分离,实现起来就和特定的业务完全无关了,所以上述的设计实现工作量其实非常小。我们的基础平台服务开发工程师,在 Get 到这个工程场景后,只用了一天时间就完成了开发和上线工作,给迭代时间本来就很紧的业务开发团队提供服务,让业务产品可以非常敏捷和优雅地实现各种类似功能。

另外,在开源的 Netty 代码库中,有一个 HashedWheelTimer,其实现原理也是类似的,大家可以参考源代码:

https://github.com/netty/netty/blob/master/common/src/main/java/io/netty/util/HashedWheelTimer.java

如何优雅地实现一个闹钟服务相关推荐

  1. Android闹钟服务AlarmManager

    获取服务 AlarmManager mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 设置闹钟 Andro ...

  2. Python:一个闹钟

    之前我做的程序,一个使用了Tkinter库,一个则是Pygame,总之都是带有图形化的界面的.但作为一个懒汉,我自然能懒必懒(这点我非常有自知之明),这次,我就来一个简单朴素的没有图形界面的程序. 这 ...

  3. android 闹钟服务,android开发笔记之 AlarmManager(闹钟服务)

    手机闹钟服务AlarmManager AlarmManager通常用来开发手机闹钟,并且它是一个全局定时器,可在指定时间或指定周期启动其他组件(包括Activity,Service,Broadcast ...

  4. android 闹钟服务,Android Service实现闹钟

    1.MainActivity.class中开启服务 import android.content.Intent; import android.support.v7.app.AppCompatActi ...

  5. 安卓AlarmManager(闹钟服务)

    Android 基础入门教程 1.0 Android基础入门教程1.0.1 2015年最新Android基础入门教程目录1.1 背景相关与系统架构分析1.2 开发环境搭建1.2.1 使用Eclipse ...

  6. 使用模式创建一个面向服务的组件中间件

    引言 在本文中,您将了解面向服务的组件中间件在用于资源有限的语音设备时,在设计阶段所应用的模式.它涵盖了项目的问题上下文,并被看成是一组决定因素,是对相关体系结构远景的一个简要概括.您还会得到一份描述 ...

  7. 基于SpringBoot开发一个Restful服务,实现增删改查功能

    点击上方"方志朋",选择"置顶公众号" 技术文章第一时间送达! 作者:虚无境 cnblogs.com/xuwujing/p/8260935.html 前言 在去 ...

  8. 快速搭建一个网关服务,动态路由、鉴权看完就会(含流程图)

    [文章来源]https://sourl.cn/tcbSPi 前 言 本文记录一下我是如何使用Gateway搭建网关服务及实现动态路由的,帮助大家学习如何快速搭建一个网关服务,了解路由相关配置,鉴权的流 ...

  9. 如何优雅的设计一个告警系统?远没有你想的那么简单!

    作者:taowen https://segmentfault.com/a/1190000003021919 目录: 告警的本质 告警对象 监控的指标和策略 理论与现实 异常检测 基于曲线的平滑性检测 ...

最新文章

  1. Camstasia studio渲染(生成)视频
  2. Ext分区文件恢复工具extundelete
  3. oracle会话状态,oracle中会话的状态
  4. 什么是时间导数(Time derivative)
  5. 学习笔记(四)——JavaScript(一)
  6. SQL Server 系统存储过程
  7. Ruby入门之零基础如何学ruby以及ruby的应用/快速学习ruby/学习ruby的流程是什么?...
  8. Oracle 左连接、右连接、全外连接、(+)号作用
  9. 玩物得志Java笔试题_代码规范利器-CheckStyle
  10. XADD和NEG命令
  11. 安装m2eclipse插件
  12. 帮你深度探寻Spring循环依赖源码实现!面经解析
  13. 八数码深度优先搜索_树的深度优先搜索(上)
  14. 【Android】在有menu键的手机上显示ActionBar上的Menu键
  15. 排序算法-冒泡算法【GIF图解】初学者小白必看
  16. 微信共享智能充电桩小程序开发功能方案
  17. c语言对随机数进行快速排序,C语言快速排序与二分查找算法示例
  18. U盘制作启动盘后无法使用,显示无法格式化等信息
  19. 赋予DBLINK权限
  20. 你所浪费的今天,是昨天死去的人奢望的明天。你所厌恶的现在,是未来的你回不去的曾经

热门文章

  1. python分析彩票_网易彩票推介西甲联赛第15轮:马拉加VS努曼西亚
  2. 基于 openEuler 22.09 版本构建的 NestOS 全新发布
  3. 电声乐器的演奏特征与制作技巧-----(1)打击乐篇
  4. php百度推送代码,织梦程序百度php主动推送代码,亲测可用!
  5. mysql错误码与标识
  6. 华升股份:参股湘财证券 洼地井喷
  7. 小戴媒体播放器4 1.37
  8. 只有做到这三点,你的产品才可能成功
  9. Spring 持久化笔记(JdbcTemplate、Mybatis、Redis)
  10. 谷歌翻译浏览器扩展,替换国内服务器版