redis 亿级别的key-value 存储查询实践

1. 背景

最近项目中有使用到redis,需要存储的数据是key-value类型的,为了优雅的使用redis,提高查询效率,学习了一下,这里记录使用到的redis相关知识,主要涉及:

  • redis的pipline插入,
  • redis hash键值优化存储
  • 分桶存储海量数据
  • 数据定时删除
  • 其他

下面我将使用n-gram来进行相关实验,key是n_gram, value是其频率,n从0取到5,使用维基百科中文数据,最后的key值总共能达到10亿+的规模;
redis的版本是5.x, 运行在Centos7上,内存64G;
编程使用的语言采用python;

2. redis pipline 插入

这里我们简单的使用hash作为底层的存储结果,将不同n的gram分别存储到一个hash表中, ngram存储在文件中,例如bigram的文件格式如下:

北京 100
上海 100

每行是一个ngram信息,使用空格切分,第一个是ngram, 第二个是其频率。

插入到redis的代码如下:

def insert_n_gram_to_redis(name, host, port, db, path):
    """
    往redis中插入n gram
    :param name: hash 表的名称
    :param host:  数据库的ip
    :param port:  端口
    :param db:  db号
    :param path: 存储ngram的文件目录
    :return: 
    """
    pool_db1 = redis.ConnectionPool(host=host, port=port, db=db)
    conn = redis.Redis(connection_pool=pool_db1)
    with conn.pipeline(transaction=False) as pipline:
        with open(path, 'r', encoding='utf-8') as fd:
            for idx, line in tqdm(enumerate(fd)):
                line = line.strip().split(' ')
                if len(line) == 2:
                    gram = line[0]
                    cnt = int(line[1])
                    pipline.hset(name, gram, cnt)
                if idx != 0 and idx % 100000 == 0:
                    pipline.execute()

使用pipline的插入相当于MySQL里面的batch insert, 这样的插入效率比逐条插入效率高很多
这里插入9KW的数据,36分钟完成, 我觉得还是挺快的:
在这里插入图片描述

3.redis hash键值优化存储

很明显,在存储大key的时候,字符串占用的存储空间是很多的,而int类型就不一样了,它占用的空间是固定的,而且用int作为索引类型,在MySQL中是比字符高效的,鉴于此,我们对存储的数据类型进行映射,将字符串的key,映射为int型的key。
这里有多重方法,例如
1: 使用自增的id来代表字符串,
2.使用同一的映射规则
第一种简单方便,但是在增量更新的时候,需要获取最后插入的key的id
第二种不需要历史信息,定义好映射规则,后面的全部套用就好了。
这里使用第二种方法,我使用的是md5值,然后将这些值取模后映射到指定的区间中,映射函数如下:

import hashlib

def str_hash_to_int(s, end=10):
    return int(hashlib.sha1(s.encode('utf-8')).hexdigest(), 16) % (10 ** end)

end表明最大的字符串id,例如end=10的时候,最大值是10个9,可以存储10亿的数据。
这样,使用int型代替str类型的key操作完成了。

4.分桶存储海量数据

参考大佬的blog:Redis百亿级Key存储方案, 在存储超大的key,value的时候,redis内存消耗是很大的,为了节约内存,blog中提出一种桶的方法来进行操作,即把hash函数的值域小于数据大小,这样就会存在部分的key出现相同的hash值(碰撞), 把相同hash值的数据聚合存储在一个hash结构中,hash只作为hash表的名称。
为了节约hash表名称的存储大小,作者还将hash结果使用bit方式使用合适的位数进行存储。blog中使用的是java,笔者照葫芦画瓢,使用python写出了hash 表名的位运算结果:

def get_bucket_id(key:bytes, bit:int=27):
    """
    获取hash 桶 get(key1) -> hget(md5(key1), key1) 从而得到value1
    reference:https://www.cnblogs.com/williamjie/p/11124228.html
    :param key:
    :param bit:
    :return:
    """
    hash = hashlib.md5()
    hash.update(key)
    md = bytearray(hash.digest())
    r = [0] * ((bit-1)//7 + 1)
    a = pow(2, bit%7) - 2
    md[len(r)-1] = md[len(r)-1] & a
    r = copy(md[:len(r)])
    for i in range(len(r)):
        if r[i] < 0:
            r[i] &= 127
    return bytes(r)

例如我们有10亿的数据要存储,那么我们预先就规划100亿的存储(规划要比实际的大,便于扩展)每个桶保存10个值,那么我们的桶数量可以通过这样的方式计算出来:100/10 = 10亿,然后取以2为底的log,大约是30, 也就是2的30次方>=10亿
例如想算:“北京”的桶值,那么可以使用下面的方法:

print(get_bucket_id(bytes('北京', encoding='utf-8'), bit=30))

结果为:

b'i.\x92f\x00'

5.数据定时删除

在redis的python api中,定时删除在string中的api如下:

set(name, value, ex=None, px=None, nx=False, xx=False)

参数:

ex - 过期时间(秒)
px - 过期时间(毫秒)
nx - 如果设置为True,则只有name不存在时,当前set操作才执行
xx - 如果设置为True,则只有name存在时,当前set操作才执行

6.其他

6.1 删除hash表中的指定值

conn.hdel(name, key)

7.参考

1. https://www.cnblogs.com/colorfulkoala/p/5783556.html
2. https://www.runoob.com/w3cnote/python-redis-intro.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值