获取Redis中同一前缀key踩坑记录

前言

之前有一个订单购买后用户生效保护期的需求,根据当时的需求本人是将key设置好自定义前缀规则,并设置了过期时间然后统一放在了redis上面,后来需求变动,所有的保护期过期之后需要留档处理,这时候就需要把之前设置的同一前缀的保护期数据在过期之前全部转移到数据库留档,就需要将之前设置的所有保护期相关数据全部获取出来。这时的kv大概已经有了300w条左右了。

这里就有两个方案了keys * 和 scan

keys *这个虽然可以实现目的但是最好还是不要使用,因为如果线上数据过大,就会导致单线程的redis阻塞,长时间无法处理后续请求,然后你就等着被捶吧。

scan可以理解是keys 的分批次执行,简单来说就是会有一个游标记录一次扫描的结束位置,然后拿着这个游标作为下一次扫描的起始位置,反复执行,每次执行数据量不大而且每次执行时间也足够短,直到扫描完整个redis库,全程不会造成redis的无响应状况,但是需要声明一点,*这个scan操作是会出现重复key值的,所以需要业务侧做好去重处理哈。本文的重点!为什么说这个scan会出现重复值,又为什么这个scan是不会漏值的?**下面就来说说本人的理解。

实际使用

下面是本人在项目中的一些用法,代码实现功能就是将自己需要的一些kv值遍历出来放进mysql进行存储,由于线上数据量较大,整个过程大约持续了30分钟,但是redis服务全程无异常状况,完美达到目的。

核心代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
rcnn := redis.Connection()
begin := time.Now().Unix()
firstStrs, cursor := rcnn.Scan(0, "xxxxx_service_xxxxx*", 1000).Val()
fmt.Println(time.Now().Unix() - begin)
for _, value := range firstStrs {
key := value
keyValue := rcnn.Get(key).Val()
ttlSeconds := int64(rcnn.TTL(key).Val().Seconds())
tnow := time.Now().Unix()
memberId, _ := strconv.Atoi((strings.Split(key, "&")[1]))
orderId := strings.Split(keyValue, "&")[0]
pPeriod := &protectperiod.ProtectPeriod{
MemberId: int64(memberId),
OrderId: orderId,
Content: keyValue,
CreatedAt: tnow + ttlSeconds - 3628800,
ExpiredAt: tnow + ttlSeconds,
Status: "0",
Source: "redis",
}
cErr := pPeriod.CreateNewRecord()
if cErr != nil {
fmt.Println(cErr)
}
}
for cursor != 0 {
cbegin := time.Now().Unix()
strs, nextCursor := rcnn.Scan(cursor, "xxxxx_service_xxxxx*", 1000).Val()
fmt.Println(time.Now().Unix() - cbegin)
for _, value := range strs {
key := value
keyValue := rcnn.Get(key).Val()
ttlSeconds := int64(rcnn.TTL(key).Val().Seconds())
tnow := time.Now().Unix()
memberId, _ := strconv.Atoi((strings.Split(key, "&")[1]))
orderId := strings.Split(keyValue, "&")[0]
pPeriod := &protectperiod.ProtectPeriod{
MemberId: int64(memberId),
OrderId: orderId,
Content: keyValue,
CreatedAt: tnow + ttlSeconds - 3628800,
ExpiredAt: tnow + ttlSeconds,
Status: "0",
Source: "redis",
}
cErr := pPeriod.CreateNewRecord()
if cErr != nil {
fmt.Println(cErr)
}
}
cursor = nextCursor
}
fmt.Println("all Done")

原理

好了,在前言中有说到为什么说这个scan会出现重复值,又为什么这个scan是安全不会漏值的? 在讨论这个问题之前,我们需要先了解什么是Rehash,学过java的都知道hashmap,当hashmap中的槽不够用的时候就会在每一个槽产生过长的链表导致效率低下等问题,在hash冲突严重的时候,hashmap就会进行扩容,然后把原来的kv值转移到新的扩容的好hashmap中去,这个过程其实就是rehash。其实在redis中这一过程准确点的叫法是渐进式rehash,点此处了解什么是渐进式rehash, 了解完上述概念之后,我们现在再来看看scan命令为什么是安全不漏值而且可能会出现重复值的。

总结

如果遇到线上redis需要获取大批量的kv值,用scan不会错。切记客户端一定要做好去重处理!!!