1. 前言
最近项目中需要计算终端在线留存率。
终端在线信息存储于Redis的Bitmaps数据结构中,设想通过对Bitmaps执行BitOP AND操作求出在多个指定日期在线的终端数量,从而计算在线率。
2.问题及解决方案
在实际操作过程中,发现对不同的Bitmaps执行BitOP AND操作出现如下错误提示信息:
> bitop and terminal:retention:day:2024-04-26 terminal:retention:day:2024-04-28
CROSSSLOT Keys in request don't hash to the same slot
出现此问题的原因在于terminal:retention:day:2024-04-26
以及terminal:retention:day:2024-04-28
两个Key位于不同的槽位上,Redis Cluster无法对处于不同槽位的Bitmaps执行BItOP操作。
查询Key所处槽位命令如下:
> CLUSTER KEYSLOT terminal:retention:day:2024-04-26
11514
> CLUSTER KEYSLOT terminal:retention:day:2024-04-28
3380
2.1 Hashtag
Hashtag是一种特殊的key,其通过对key中{}引用部分字段进行hash,并根据hash数值确定key所存放的槽位,具体计算方式可参考如下代码:
unsigned int HASH_SLOT(char *key, int keylen) {
int s, e; /* start-end indexes of { and } */
/* Search the first occurrence of '{'. */
for (s = 0; s < keylen; s++)
if (key[s] == '{') break;
/* No '{' ? Hash the whole key. This is the base case. */
if (s == keylen) return crc16(key,keylen) & 16383;
/* '{' found? Check if we have the corresponding '}'. */
for (e = s+1; e < keylen; e++)
if (key[e] == '}') break;
/* No '}' or nothing between {} ? Hash the whole key. */
if (e == keylen || e == s+1) return crc16(key,keylen) & 16383;
/* If we are here there is both a { and a } on its right. Hash
* what is in the middle between { and }. */
return crc16(key+s+1,e-s-1) & 16383;
}
因此可以将上文中的key改写为terminal:retention:{day}:2024-04-26
以及terminal:retention:{day}:2024-04-28
,这样就能确保两个key处于同一槽位。
但是此方案存在弊端,即容易造成数据倾斜,例如当前项目需要存储年月日三个维度的终端在线信息,存储周期接近一年,那么相同hashtag的key均存储于某一固定槽位上,易导致此槽位所在的节点存储和计算负载较高。
2.2 内存计算
考虑需求中留存率计算是统计类任务,无需支持实时计算,时效性要求不高。因此项目最后选择使用定时任务逐个触发留存率计算,Redisson可以将Bitmaps存储数据拷贝至应用中,并支持在内存中执行AND/OR以及计数等操作。
具体可参考如下代码:
var intersectionBitSet = (BitSet) baseBitSet.clone();
intersectionBitSet.and(targetBitSet);
var intersectionCount = (long) intersectionBitSet.cardinality();
return new TerminalRetentionCalculateRes(baseCount, intersectionCount,
String.format("%.2f", intersectionCount * 100.0 / baseCount));