圆点 快速的未来即将到来,活动就在你的城市举办。

加入我们参加 Redis Released

动态电子邮件营销背后的技术

邮件营销是联系客户的有效方式,但随之而来的是各种难以克服的挑战。电子邮件的性质在发送后是静态的:一旦消息离开您的电子邮件服务器,您几乎无法控制——或者您可以吗?让我们来看看一种在 发送 电子邮件后可以提供一定程度动态控制的技术。

想象一下,您正在发送一封包含三个交易的电子邮件。您不确定哪个交易的效果最好。电子邮件发送后,您将获得非常清晰的画面——哪一个交易的点击率和频率最高。 从众心理在电子商务中是一个很真实的事情。告诉用户某个商品“畅销”或“热门”可以让他们对该商品产生信心,我们可以通过“徽章”来传达这一点。但若要有效,它应该实时显示,而不是来自以前的周期。

为了实现此目的,我们将使用 Redis 和 Node.js 服务器以及通常用于发送电子邮件的任何工具。此技术的核心是将您的徽章图片 URL 和商品 URL 指向中间 URL,而不是指向实际图片或网页。

中间徽章 URL 会使用来自 Redis 的数据计算商品的“热门度”和受欢迎程度,然后向电子邮件客户端发送 HTTP 307 转发请求。转发指向正确的徽章图片——一个用于热门,一个用于流行,一个特殊的空/透明图片用于既不热门也不流行的商品。

商品中间 URL 稍微简单一些——此 URL 会记录访问发生在特定时间,并为点击次数计数器增加一个计数。一旦完成,它总是转发到相同的目标 URL。

以下是对整个过程的图解

它比看上去简单。

我们如何计算热门度

在本脚本中,如果商品在过去几分钟内被访问过几次,则该商品被视为“热门”。为了确定“热门度”,我们统计了分钟数和位数。

Redis 能够翻转字符串中的各个位,我们可以利用此功能在极小存储空间中极其细化。

每个“商品”都由 Redis 中的一个字符串表示,该字符串的键源自广告活动和某种商品标识符。我们会让键看起来像这样

deals:august17:camera
^^^^^ ^^^^^^^^ ^^^^^^
|     |        |----> the item ID is "camera"
|     |----> the campaign is "august17"
|----> The "root" of the key

我们需要从某一固定的时间点开始计数——就如同 UNIX 时代时间系统一样,我们只要使用某个时间点即可。对于这个简单的示例,我们选择 2017 年 7 月 1 日午夜(格林尼治时间)——计数纪元。我们将使用 date-utils Node.js 模块将此时间与当前时间戳进行比较。

minutesSinceEpoch = countEpoch.getMinutesBetween(new Date())

举个例子,2017 年 7 月 1 日凌晨 1 点时,minutesSinceEpoch 为 59(不是 60,因为是从 0 开始计数的)。每次有用户与一项内容进行交互时,我们就翻转一位。请注意,如果在同一分钟期间,两位用户与同一项内容进行了交互,则只计算一次——在这种情况下,我们获取的是活动,而不是次数。这样非常节省空间,而且可以以极小的存储空间获取极丰富的数据。从 计数纪元 算起,每天耗费 180 字节,每月大约 5.4kb,每年大约 65kb。不错吧。

为了翻转位,我们可以使用 Redis 函数 SETBIT,其中偏移量是 minutesSinceEpoch 和值为 1,表示访问。以上述示例(7 月 1 日凌晨 1 点)为例,我们的 Redis 命令看起来如下

> SETBIT deals:august17:camera 59 1

SETBIT 是一个计算负担不大的命令,O(1) 并且在应用层,只需要做一点减法,就可以获取 minutesSinceEpoch.

查询“热门程度”位需要做一些额外的工作。我们可以使用 Redis 命令 BITCOUNT。借助此命令,我们可以找到给定字符串中设置的 1 的数量。虽然这本身就有用,但我们希望将其添加为最近的时间——对于我们的应用程序来说,确定在两个月之前与该项内容进行了交互是不相关的——我们需要找到最近的活动。通过 BITCOUNT,我们可以提供可选的 start 和 end 参数,只提取一部分数据。start 和 end 参数还以负数“环绕”,因此你只能统计数据的末尾部分的位。在我们的示例中,让我们使用最后的三字节

> BITCOUNT deals:august17:camera -3 -1

虽然此命令设置了位,但了解 Redis 只处理 BITCOUNT 范围参数中的字节非常重要。这会给计算带来一些不准确性:考虑一下在下方的示例中设置第 17 个字节的情况

01234567|01234567|01234567
--------+--------+--------
00000110|00001010|1

运行以上命令将计算最后 3 个字节的五个 1 位,这不完全是 24 分钟,因为 3 个字节可能让你产生了这样的想法。因此,我们实际上是想说最后 16-24 分钟中有 5 分钟是活跃的。在我们的用例中,我们实际上只是想获得一些相对的热门程度,而不是绝对程度,所以考虑到空间/时间的效率,这是可以接受的。

计算热度

在我们的用例中,我们将找到点击次数最多的商品,而不管时间如何。这是 Redis 中的一个简单过程。有序集合数据结构非常适合创建排行榜  — 我们的热度是通过在组中的全部商品中找到排名前列的商品来计算的。

要记录热度,我们只需要每次点击某个商品时增加 ZSET 中该商品的分数。我们可以通过使用ZINCRBY命令来实现。整个活动有一个单密钥,成员是商品 ID。我们想将它增加一。在 Redis 中,我们可以做这样的事

> ZINCRBY deals:pop:august17 1 camera

说清楚,分数只是充当一个计数器。因此,如果august17活动中的camera商品被点击了 47 次,第一天点击 46 次,第 400 天再点击一次,则分数将为 47。

对于每个徽章,我们都会检查相关的itemId是否是最流行的。我们可以通过运行以下命令来完成此操作

> ZREVRANGE deals:pop:august17 0 0

如果此响应与itemId匹配,则该商品是最流行的。

性能问题

正如你从流程图中看到的,这会产生一个非常“健谈”的系统,其中包含多个活动部件。HTTP 转发增加了额外的网络往返时间和额外传输时间。在记录和计算热度时,必须最大限度地减少花费的时间。Redis 以低延迟和快速计算值而闻名。

一次将这种类型的电子邮件发送到众多客户端的动态也会带来挑战。假设你的电子邮件在短时间内发送给 500,000 个收件人,他们开始同时与电子邮件互动并查看电子邮件。使用单个 Redis 实例,你可能可以度过难关,但服务器的单线程性质可能会在负载下产生高于预期的延迟。此外,我们正在创建一个单点故障点,这是有风险的。Redis 企业版非常适合此用例。Redis 企业版可提供高可用性和自动故障转移,以确保你的电子邮件资产没有单点故障,它还可以提供集群,可在多个线程和/或机器之间分散负载,从而降低延迟。

后续步骤和其他用途 

除其他用例外,还可以利用数据。利用 BITCOUNT 根据基于计算分钟的偏移量,可以计算“热度”数据(即位图),从而查看给定各项在给定时间范围内有几小时的活动。也可以使用 BITOP 聚合多项的活动。

> BITOP AND camera-and-watch deals:august17:camera deals:august17:watch

然后可以切出 camera-and-watch 的规定时间段,以确定时间段的热度。

将 Node 和 Redis 作为数据交互点和资产进行配合的技术不仅限于发送电子邮件图像,还可以将其集成到任何平台当中,以便在数据发生变化时动态更改图像。在当前状态下,它可以应用于电子商务系统,几乎不必修改即可传达相同的“标记”信息。通过与其他数据集成,可以基于“热度”和流行度,以及库存数据(“即将售罄”)或折扣(“优惠 20%”)等多种变量来改变最终标记。此外,与用户系统配对,它可能有利于个性化特价(“重新下单”或“我们的推荐”)。甚至可以想象集成客户位置的情况(如果客户所在位置比较暖和,那么空调上可以显示“酷夏也能击败”)。

源代码可以在 Github 上找到。

(这篇文章最初出现在 Node/Redis 系列的 Medium 上)