Redis

Scan 命令代码示例

有一些需求需要我们扫描 redis 的所有键值,如果用 keys 会阻塞redis 非常危险,推荐用 scan 命令。如果需要扫描一个hash/zset 等也有对应的 hscan, zscan 等命令可以使用。

  • 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
  • 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
  • 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;

参考:https://www.lixueduan.com/post/redis/redis-scan/ Redis Scan 原理解析与踩坑

import redis  # pip install redis


def init_redis():
    HOST, PORT, PWD = "host", 6379, "pwd"  # 用你的 redis 配置替代,最好读取配置不要硬编码密码等保证安全
    return redis.Redis(host=HOST, port=PORT, password=PWD)


def get_all_node_ids(r):
    """ 获取所有的腾讯云 redis 集群 master node_id。扫描的需要覆盖所有 master 节点。如果scan 一个复合结构不需要扫所有节点
    redis cluster 架构
    u'ip:port@12028': {u'connected': True,                                                                                                                                                                                     [149/176]
                             u'epoch': u'17',
                             u'flags': u'master',
                             u'last_ping_sent': u'0',
                             u'last_pong_rcvd': u'1580544851000',
                             u'master_id': u'-',
                             u'node_id': u'XXXXXXXXXXXXX',
                             u'slots': [[u'8192', u'10239']]},
    """
    from collections import defaultdict
    node_dict = r.execute_command("cluster nodes")
    master_slaves = defaultdict(list)  # {master_id : [slave_ids]}
    for _addr_id, info in node_dict.items():
        if info.get("flags", "") == "slave":
            master_id = info["master_id"]
            master_slaves[master_id].append(info["node_id"])
    master_ids = list(master_slaves.keys())
    slave_ids = []  # 获取其中一个 slave id
    for _, slave_ids in master_slaves.items():
        slave_ids.append(slave_ids[0])
    return master_ids, slave_ids


def scan(r, node_id='', cursor=0, match=None, count=None):
    pieces = [cursor]
    if match is not None:
        pieces.extend([b'MATCH', match])
    if count is not None:
        pieces.extend([b'COUNT', count])
    if node_id:
        pieces.append(node_id)
    return r.execute_command('SCAN', *pieces)


def scan_playcount(r):  # r redis client
    _master_ids, slave_ids = get_all_node_ids(r)
    num = 0
    for node_id in slave_ids:  # scan 每一个 redis slave 节点
        cursor = 0  # reset cursor if error

        while True:
            cursor, keys = scan(r, node_id, cursor, "UR_*", 10000)  # 这里 match 设置你需要的前缀
            for key in set(keys):  # 注意如果不是幂等的,这里可能重复,需要去重
                print(key)  # 这里根据你的需求处理 key

            if cursor == 0:  # 这里说明没有多余数据了,退出
                break
    print("all nums:{}".format(num))


def main():
    redis_client = init_redis()
    scan_playcount(redis_client)


if __name__ == '__main__':
    main()

也可以 scan 单个复合结构, golang 代码示例如下(不用获取所有 master 节点了,只有一个 key):

// 遍历数据源  redis zset 获取点赞数
func getRedisZsetNum(key string) ([]string, error) {
    rc := storage.LikeRedisClient // redigo/redis 的 client
    iter := 0
    var memberScores []string
    for {
        arr, err := redis.Values(rc.Do("ZSCAN", key, iter, "MATCH", "*"))
        if err != nil {
            return nil, fmt.Errorf("key:%s error retrieving '%s' keys", key, "*")
        }

        iter, err = redis.Int(arr[0], nil)
        k, err := redis.Strings(arr[1], nil) // k  [m1 score1 m2 score2]

        logger.Debugf("redis scan key:%s k:%+v, err:%v", key, k, err)
        memberScores = append(memberScores, k...)

        if iter == 0 {
            break
        }
    }
    return memberScores, nil
}