Redis BigKey优化与使用方式

一、什么是BigKey

在Redis中,一个字符串最大512MB,一个二级数据结构(例如hash、list、set、zset)可以存储大约40亿个(2^32-1)个元素,但实际上中如果下面两种情况,我就会认为它是bigkey。 Redis中的大key一直是重点需要优化的对象,big key既占用比较多的内存,也可能占用比较多的网卡资源,造成redis阻塞,因此我们需要找到这些big key进行优化

  1. 字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey。
  2. 非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多。

二、BigKey危害

1.超时阻塞(慢查询)
由于Redis单线程的特性,操作bigkey的通常比较耗时,也就意味着阻塞Redis可能性越大,这样会造成客户端阻塞或者引起故障切换,它们通常出现在慢查询中。

2.网络拥塞
bigkey也就意味着每次获取要产生的网络流量较大,假设一个bigkey为1MB,客户端每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器是不堪重负的。

3.过期删除阻塞
有个bigkey,它安分守己(只执行简单的命令,例如hget、lpop、zscore等),但它设置了过期时间,当它过期后,会被删除,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞Redis的可能性,而且这个过期删除不会从主节点的慢查询发现(因为这个删除不是客户端产生的,是内部循环事件,可以从latency命令中获取或者从slave节点慢查询发现)。

4.迁移困难(迁移中阻塞)
Redis 部署方式为 redis cluster的并迁移slot,当实际上是通过migrate命令来完成的,migrate实际上是通过dump + restore + del三个命令组合成原子命令完成,如果是bigkey,可能会使迁移失败,而且较慢的migrate会阻塞Redis。

5.内存空间不均匀
在 Redis cluster集群中,会造成节点的内存使用不均匀。存在丢失数据的隐患。

总结:

  • bigkey 的读写操作会阻塞线程,降低 Redis 的处理效率
  • 在 Redis 基本 IO 模型中,主要是主线程在执行操作,任何耗时的操作,例如 bigkey、全量返回等操作,都是潜在的性能瓶颈。
  • AOF 重写过程中:主进程 fork 出后台的子进程会阻塞住子进程,阻塞时间取决于整个实例的内存大小。当主线程收到新写或修改的操作时,主线程会申请新的内存空间,用来保存新写或修改的数据,如果操作的是 bigkey,也就是数据量大的集合类型数据,那么主线程会因为申请大空间而面临阻塞风险。因为操作系统在分配内存空间时,有查找和锁的开销,这就会导致阻塞。

三、BigKey的产生场景

一般来说,bigkey的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的,来看几个例子:

  • 社交类:粉丝列表,如果某些明星的粉丝数据,如果不精心设计下,一个明星的粉丝 百万很少了吧,你都把这百万的>粉丝数据放到一个key中存储,毫无疑问是bigkey
  • 统计类:比如按天存储某项功能或者网站的用户集合,用户很少,倒是没多大问题,一旦用户多了起来,必是bigkey
  • 缓存类:将数据从数据库加载出来以后序列化放到Redis里,这个方式非常常用,但有两个地方需要注意,
    第一,是不>是有必要把所有字段都缓存;
    第二,有没有相关关联的数据,不要为了图方便把相关数据都存一个key下,产生bigkey。

四、BigKey发现

  • 使用 redis-cli 客户端的命令 --bigkeys
  • 生成 rdb 文件,离线分析 rdb 文件。比如:redis-rdb-cli,rdbtools;
  • 通过 scan 命令,对扫描出来的key进行类型判断,例如:string长度大于10K,list长度大于10240认为是big bigkeys

五、如何优化BigKey

核心思想: 分治 拆分


  • big list: list1、list2、…listN
    big hash:可以讲数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成 200个key,每个key下面存放5000个用户数据
  • 控制key的生命周期
    建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期)。
  • 如果无法避免使用BigKey
    可以使用其他的存储形式,文档性数据库 MongoDB。

六、BigKey删除

对于非字符串的bigkey,比如 hash list set zset , 不要使用del 删除, 请使用 hscan 、sscan、zscan方式渐进式删除。
同时要注意防止bigkey过期时间自动删除问题(例如一个100万的hash设置1小时过期,会触发del操作,造成阻塞)

  1. 字符串
    一般来说,对于string类型使用del命令不会产生阻塞。
del bigkey
  1. hash
    使用hscan命令,每次获取部分(例如100个)field-value,在利用hdel删除每个field(为了快速可以使用pipeline)。

public void delBigHash(String bigKey) {
	Jedis jedis = new Jedis("127.0.0.1", 6379);
	// 游标
	String cursor = "0";
	while(true) {
		ScanResult<Map.Entry<String, String>> scanResult = jedis.hscan(bigKey, cursor, new ScanParams().count(100));
		// 每次扫描后获取新的游标
		cursor = scanResult.getStringCursor();		// 获取扫描结果
		List<Entry<String, String>> list = scanResult.getResult();		if(list == null || list.size() == 0) {
			continue;		}		String[] fields = getFieldsFrom(list);		// 删除多个field
		jedis.hdel(bigKey, fields);		// 游标为0时停止
		if(cursor.equals("0")) {
			break;
		}	}	// 最终删除key
	jedis.del(bigKey);
}
/**
 * 获取field数组 */
private String[] getFieldsFrom(List<Entry<String, String>> list) {
	List<String> fields = new ArrayList<String>();
	for (Entry<String, String> entry : list) {
		fields.add(entry.getKey());
	}
	return fields.toArray(new String[fields.size()]);


3.list
Redis并没有提供lscan这样的API来遍历列表类型,但是提供了ltrim这样的命令可以渐进式的删除列表元素,直到把列表删除。

public void delBigList(String bigKey) {
	Jedis jedis = new Jedis("127.0.0.1", 6379);
	long llen = jedis.llen(bigKey);
	int counter = 0;
	int left = 100;
	while(counter < llen) {
		// 每次从左侧截掉100个
		jedis.ltrim(bigKey, left, llen);
		counter += left;
	}
	// 最终删除key
	jedis.del(bigKey);
}
  1. set
    使用sscan命令,每次获取部分(例如100个)元素,在利用srem删除每个元素。
public void delBigSet(String bigKey) {
	Jedis jedis = new Jedis("127.0.0.1", 6379);
	// 游标
	String cursor = "0";
	while(true) {
		ScanResult<String> scanResult = jedis.sscan(bigKey, cursor, new ScanParams().count(100));
		// 每次扫描后获取新的游标
		cursor = scanResult.getStringCursor();		// 获取扫描结果
		List<String> list = scanResult.getResult();		if(list == null || list.size() == 0) {
			continue;
		}				jedis.srem(bigKey, list.toArray(new String[list.size()]));
		// 游标为0时停止
		if(cursor.equals("0")) {
			break;
		}	}	// 最终删除key
	jedis.del(bigKey);}
  1. sorted set
    使用zscan命令,每次获取部分(例如100个)元素,在利用zremrangebyrank删除元素。
public void delBigSortedSet(String bigKey) {
	long startTime = System.currentTimeMillis();	Jedis jedis = new Jedis(HOST, PORT);	// 游标
	String cursor = "0";
	while(true) {
		ScanResult<Tuple> scanResult = jedis.zscan(bigKey, cursor, new ScanParams().count(100));
		// 每次扫描后获取新的游标
		cursor = scanResult.getStringCursor();		// 获取扫描结果
		List<Tuple> list = scanResult.getResult();		if(list == null || list.size() == 0) {
			continue;		}		String[] members = getMembers(list);		jedis.zrem(bigKey, members);		// 游标为0时停止
		if(cursor.equals("0")) {
			break;
		}	}	// 最终删除key
	jedis.del(bigKey);
}
public void delBigSortedSet2(String bigKey) {
	Jedis jedis = new Jedis(HOST, PORT);
	long zcard = jedis.zcard(bigKey);
	int counter = 0;
	int incr = 100;
	while(counter < zcard) {
		jedis.zremrangeByRank(bigKey, 0, 100);
		// 每次从左侧截掉100个
		counter += incr;
	}
	// 最终删除key
	jedis.del(bigKey);
}

七、key设计

(1)【建议】: 可读性和可管理性

以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id
o2o:order:1

(2)【建议】:简洁性

保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:
user:{uid}:friends:messages:{mid} 简化为 u:{uid}🇫🇷m:{mid}

(3)【强制】:不要包含特殊字符

反例:包含空格、换行、单双引号以及其他转义字符

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值