dot Redis 8 已发布——而且它是开源的

了解更多

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

电子邮件营销是接触客户的有效方式,但它也带来了一系列难以克服的挑战。电子邮件一旦发送出去,其性质就是静态的:消息离开电子邮件服务器后,你几乎无法控制——或者说,真的无法控制吗?让我们来看看一种技术,它可以在你发送电子邮件之后提供一定程度的动态控制。

想象一下你正在发送一封包含三个优惠的电子邮件。你不确定哪些优惠的效果最好。电子邮件发送后,你将非常清楚——哪些优惠具有最佳的点击率和频率。从众心理在电子商务中是非常真实存在的。告诉用户某个商品是“热门”或“流行”可以增强他们对该商品的信心,我们可以通过一个“徽章”来传达这一点。但要有效,它应该是实时的,而不是来自上一个周期的信息。

为了实现这一点,我们将使用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日午夜(GMT)——countEpoch。我们将使用date-utils Node.js模块将其与当前时间戳进行比较。

minutesSinceEpoch = countEpoch.getMinutesBetween(new Date())

举例来说,2017年7月1日凌晨1点,minutesSinceEpoch将是59(不是60,因为是基于零计数)。每次有人与商品互动时,我们将翻转一个比特。注意,如果两个用户在同一分钟内与同一个商品互动,只会计数一次——在这种情况下,我们得到的是活动状态而不是计数。这非常节省空间,并以极少的存储空间提供了非常丰富的数据。countEpoch之后每天会消耗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个字节是24分钟。所以,我们实际上是说,在过去的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 Enterprise会是一个很好的选择。Redis Enterprise可以提供自动故障转移的高可用性,以确保你的电子邮件资产没有单点故障,它还可以提供集群功能,可以将负载分散到多个线程和/或机器上,从而降低延迟。

下一步和其他 用途

除了这些其他用例之外,这些数据还可以以其他方式利用。“热门度”数据是一个位图,可以使用基于计算出的分钟偏移量,通过BITCOUNT计算出给定商品在给定时间范围内的活动时长。你还可以使用BITOP聚合多个商品的活动

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

然后你可以截取camera-and-watch的给定时间段来确定该时间段的热门度。

将Node和Redis置于互动点和资产之间进行协调的技术不仅限于提供电子邮件图像,还可以集成到任何平台中,根据数据变化动态更改图像。在其当前状态下,只需很少的修改就可以应用于电子商务系统,提供相同的“徽章”信息。与其他数据集成可以根据“热门度”和流行度之外的更多变量来改变生成的徽章,例如商品库存(“快卖完了!”)或折扣(“节省20%”)。此外,与用户系统配对后,它非常适合提供个性化优惠(“再次订购”或“为您推荐”)。甚至可以想象集成客户位置条件(如果客户位置温暖,则在空调上显示“击退高温”)。

源代码可在Github上找到。

(此文最初发表在Medium上的Node / Redis系列中)