2团
Published on 2024-08-15 / 12 Visits
0
0

Redis Cluster执行BitOP提示CROSSSLOT

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));


Comment