作者简介
马听,多年 DBA 实战经验,对MySQL、 Redis、MongoDB、Go等有一定了解,书籍《MySQL DBA精英实战课》作者,慕课网DBA体系课(https://class.imooc.com/sale/dba)讲师。
有时候,可能因为业务设计不合理,或者其他的一些原因,导致Redis某个key非常大。
而在查询这类大key的时候,往往响应比较慢,并且也会影响其他正常查询。
所以,我们应该关注Redis里面的key详情,及时发现大key并治理,提前预防大key产生的影响。
1 什么是 Bigkey
下面这两种情况,在很多互联网公司都被认为是 Bigkey:
字符串类型超过10KB
当然,这也不是硬性规定,主要看我们的业务场景
非字符串类型元素个数超过5000个
比如哈希、列表、集合、有序集合,元素个数超过 5000 的,就可以认为是Bigkey。
2 Bigkey的危害
Bigkey 存在很多危害,具体体现在以这些方面:
内存空间不均匀
比如在 Redis cluster 或者其他集群架构中,会造成节点的内存使用不均匀。
查询时阻塞
因为 Redis 单线程特性,如果操作某个 Bigkey 耗时比较久,则后面的请求会被阻塞。
过期时阻塞
如果 Bigkey 设置了过期时间,当过期后,这个 key 会被删除,假如没有使用过期异步删除,就会存在阻塞 Redis 的可能性,并且慢查询中查不到(因为这个删除是内部循环事件)。
3 用ChatGPT编写制造Bigkey的程序
构造一些大key,方便我们等下测试。
发送给ChatGPT,提示语为:
帮忙写一个Go程序 往Redis里面写入一些测试的key string类型大小超过10kbkey hash、list、sets、zsets等key,元素超过5000 key的名字都是以large开头的 Redis的IP和端口192.168.12.161:7001,密码:xxx
生成的程序如下:
编辑一个文件,writeBigkey.go
代码语言:javascript复制package main
import (
"context"
"fmt"
"log"
"math/rand"
"strconv"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
// 创建Redis客户端连接
client := redis.NewClient(&redis.Options{
Addr: "192.168.12.161:7001", // Redis服务器地址
Password: "xxx", // 如果有密码,请填写密码
DB: 0, // 默认数据库
})
// 使用完后关闭连接
defer client.Close()
// 写入字符串类型的键,大小大于10KB
largeStringValue := generateLargeString(10240) // 生成大于10KB的字符串
err := client.Set(context.Background(), "large_string_key", largeStringValue, 0).Err()
if err != nil {
log.Fatalf("Error writing large string key: %v", err)
}
// 写入哈希类型的键,元素个数大于5000
hashData := make(map[string]interface{})
for i := 0; i < 6000; i {
field := fmt.Sprintf("field%d", i)
value := fmt.Sprintf("value%d", i)
hashData[field] = value
}
err = client.HMSet(context.Background(), "large_hash_key", hashData).Err()
if err != nil {
log.Fatalf("Error writing large hash key: %v", err)
}
// 写入列表类型的键,元素个数大于5000
listData := make([]interface{}, 6000)
for i := 0; i < 6000; i {
listData[i] = strconv.Itoa(i)
}
err = client.RPush(context.Background(), "large_list_key", listData...).Err()
if err != nil {
log.Fatalf("Error writing large list key: %v", err)
}
fmt.Println("Keys have been written successfully!")
}
// 生成指定大小的随机字符串
func generateLargeString(size int) string {
rand.Seed(time.Now().UnixNano())
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, size)
for i := range result {
result[i] = charset[rand.Intn(len(charset))]
}
return string(result)
}
运行程序,就可以生成一些大key。
4 找到大key
我们来讲一下几种常见的找打大key的方法
4.1 Redis客户端自带的参数查找Bigkey
执行
代码语言:javascript复制redis-cli -p 7001 -a xxx --bigkeys
大致结果和注释如下:
代码语言:javascript复制[00.00%] Biggest string found so far '"large_string_key"' with 10240 bytes
[00.00%] Biggest set found so far '"set_test"' with 1 members
[00.00%] Biggest hash found so far '"user:01"' with 1 fields
[00.00%] Biggest list found so far '"large_list_key"' with 6000 items
[00.00%] Biggest hash found so far '"large_hash_key"' with 6000 fields
-------- summary -------
Sampled 8 keys in the keyspace!
# 一共扫描了 8 个key
Total key length in bytes is 82 (avg len 10.25)
# 所有key的总长度是 82 字节,平均长度是 10.25 字节
Biggest list found '"large_list_key"' has 6000 items
Biggest hash found '"large_hash_key"' has 6000 fields
Biggest string found '"large_string_key"' has 10240 bytes
Biggest set found '"set_test"' has 1 members
# 每一种key情况的总览,某如某个类型,扫描出多少个key,占比多少,平均大小。
2 lists with 6001 items (25.00% of keys, avg size 3000.50)
2 hashs with 6001 fields (25.00% of keys, avg size 3000.50)
3 strings with 10246 bytes (37.50% of keys, avg size 3415.33)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
1 sets with 1 members (12.50% of keys, avg size 1.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
4.2 通过ChatGPT编写代码查找Bigkey
用ChatGPT编写查询Bigkey的程序:
帮忙写一个Go程序 扫描出Redis里面string类型大小超过10kb的key hash、list、sets、zsets等key,扫描出元素超过5000的key Redis的IP和端口192.168.12.161:7001,密码:xxx
生成的代码如下:
代码语言:javascript复制package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"log"
)
func main() {
// 创建Redis客户端连接
client := redis.NewClient(&redis.Options{
Addr: "192.168.12.161:7001", // Redis服务器地址
Password: "xxx", // 如果有密码,请填写密码
DB: 0, // 默认数据库
})
// 使用完后关闭连接
defer client.Close()
// 获取所有键
keys, err := client.Keys(context.Background(), "*").Result()
if err != nil {
log.Fatalf("Error getting keys: %v", err)
}
// 遍历所有键
for _, key := range keys {
keyType, err := client.Type(context.Background(), key).Result()
if err != nil {
log.Printf("Error getting key type for key '%s': %v", key, err)
continue
}
switch keyType {
case "string":
// 检查字符串类型键的大小
valueSize, err := client.StrLen(context.Background(), key).Result()
if err != nil {
log.Printf("Error getting size for string key '%s': %v", key, err)
continue
}
//fmt.Printf("String key '%s' size: %d bytesn", key, valueSize)
if valueSize >= 10240 { // 大于10KB
fmt.Printf("String key '%s' size: %d bytesn", key, valueSize)
}
case "hash":
// 检查哈希类型键的元素数量
hashSize, err := client.HLen(context.Background(), key).Result()
if err != nil {
log.Printf("Error getting size for hash key '%s': %v", key, err)
continue
}
if hashSize > 5000 {
fmt.Printf("Hash key '%s' has %d elementsn", key, hashSize)
}
case "list":
// 检查列表类型键的元素数量
listSize, err := client.LLen(context.Background(), key).Result()
if err != nil {
log.Printf("Error getting size for list key '%s': %v", key, err)
continue
}
if listSize > 5000 {
fmt.Printf("List key '%s' has %d elementsn", key, listSize)
}
case "set":
// 检查集合类型键的元素数量
setSize, err := client.SCard(context.Background(), key).Result()
if err != nil {
log.Printf("Error getting size for set key '%s': %v", key, err)
continue
}
if setSize > 5000 {
fmt.Printf("Set key '%s' has %d elementsn", key, setSize)
}
case "zset":
// 检查有序集合类型键的元素数量
zsetSize, err := client.ZCard(context.Background(), key).Result()
if err != nil {
log.Printf("Error getting size for zset key '%s': %v", key, err)
continue
}
if zsetSize > 5000 {
fmt.Printf("Zset key '%s' has %d elementsn", key, zsetSize)
}
}
}
}
4.3 通过rdbtools来找到Bigkey
大概过程就是:
通过 rdbtools 分析 rdb 生成 csv 文件,再导入 MySQL 或其他数据库中进行分析统计,根据 size_in_bytes 统计 Bigkey。
获取Redis的RDB文件
在Redis中执行
代码语言:javascript复制bgsave
创建一个rdb的分析目录
代码语言:javascript复制mkdir /data/analyserdb
把rdb复制一份到分析目录
代码语言:javascript复制cp /data/redis7001/data/dump.rdb /data/analyserdb/192.168.12.161_dump.rdb
通过rdbtools分析RDB中的key
安装Python3
代码语言:javascript复制yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel
cd /usr/src
wget https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tgz
tar -xvf Python-3.7.3.tgz
cd Python-3.7.3/
./configure
make && make install
安装 redis-rdb-tools,参考 GitHub(https://github.com/sripathikrishnan/redis-rdb-tools)。
代码语言:javascript复制yum install python3-devel
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3 get-pip.py
pip3 install python-lzf
pip3 install rdbtools
rdb --help
我们可以通过官网的Python示例获取到大key,比如
https://github.com/sripathikrishnan/redis-rdb-tools
编辑文件analyse_rdb.py
代码语言:javascript复制from rdbtools import RdbParser, RdbCallback
from rdbtools.encodehelpers import bytes_to_unicode
class MyCallback(RdbCallback):
''' Simple example to show how callback works.
See RdbCallback for all available callback methods.
See JsonCallback for a concrete example
'''
def __init__(self):
super(MyCallback, self).__init__(string_escape=None)
def encode_key(self, key):
return bytes_to_unicode(key, self._escape, skip_printable=True)
def encode_value(self, val):
return bytes_to_unicode(val, self._escape)
def set(self, key, value, expiry, info):
print('%s = %s' % (self.encode_key(key), self.encode_value(value)))
def hset(self, key, field, value):
print('%s.%s = %s' % (self.encode_key(key), self.encode_key(field), self.encode_value(value)))
def sadd(self, key, member):
print('%s has {%s}' % (self.encode_key(key), self.encode_value(member)))
def rpush(self, key, value):
print('%s has [%s]' % (self.encode_key(key), self.encode_value(value)))
def zadd(self, key, score, member):
print('%s has {%s : %s}' % (str(key), str(member), str(score)))
callback = MyCallback()
parser = RdbParser(callback)
parser.parse('./dump.rdb')
我们再来运行Python代码
代码语言:javascript复制python3 analyse_rdb.py >result.txt
也可以使用命令进行 RDB 分析
代码语言:javascript复制cd /data/analyserdb
rdb -c memory 192.168.12.161_dump.rdb >192.168.12.161_rdb_result.csv
各个字段的解释如下
- database(库编号)
- type(键类型)
- key(键名字)
- size_in_bytes(键大小)
- encoding(键编码)
- num_elements(元素个数)
- len_largest_element(最大元素大小)
- expiry(过期时间)
把分析结果写入MySQL
登录MySQL
创建一个用户
代码语言:javascript复制create database rdb;
create user 'u_rdb'@'%' IDENTIFIED WITH mysql_native_password BY '1g18_pDgnd12' ;
GRANT update,delete,insert,select ON rdb.* TO 'u_rdb'@'%' ;
创建MySQL表
代码语言:javascript复制USE rdb;
CREATE TABLE rdb_result (
id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
ip_add VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'IP Address',
key_database INT NOT NULL DEFAULT '0' COMMENT 'Database Number',
key_type VARCHAR(10) NOT NULL DEFAULT '' COMMENT 'Key Type',
key_name VARCHAR(50) NOT NULL DEFAULT '' COMMENT 'Key Name',
key_size INT NOT NULL DEFAULT '0' COMMENT 'Key Size',
key_encoding VARCHAR(10) NOT NULL DEFAULT '' COMMENT 'Key Encoding',
num_elements INT NOT NULL DEFAULT '0' COMMENT 'Number of Elements',
len_largest_element INT NOT NULL DEFAULT '0' COMMENT 'Largest Element Size',
key_expiry VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'Key Expiry Time',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Record Creation Time',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Record Update Time',
status TINYINT NOT NULL DEFAULT '1' COMMENT '1 represents a valid record, 0 represents an invalid record',
PRIMARY KEY (id),
KEY idx_stu_score (key_size)
) ENGINE = INNODB CHARSET = utf8mb4 COMMENT 'Redis RDB Analysis Result Table';
修改MySQL配置文件
代码语言:javascript复制local-infile=on
这个参数控制是否允许从客户端本地文件加载数据到 MySQL 服务器中
重启MySQL
登录MySQL
代码语言:javascript复制mysql -uroot -p --local-infile=1
导入数据
代码语言:javascript复制use rdb;
LOAD DATA LOCAL INFILE '/data/analyserdb/192.168.12.161_rdb_result.csv'
INTO TABLE rdb_result
FIELDS TERMINATED BY ','
LINES TERMINATED BY 'n'
IGNORE 1 LINES
(key_database, key_type, key_name, key_size, key_encoding, num_elements, len_largest_element, key_expiry)
SET ip_add = '192.168.12.161';
比如我们想找到bigkey,可以执行语句
代码语言:javascript复制select * from rdb_result where key_size > 10240;
5 优化Bigkey
找到 Bigkey 后,怎么优化呢?
这里介绍几种常见的优化方式:
删除不用的Bigkey
有些 Bigkey 业务不需要使用了,因此可以考虑删除掉。但是要注意的是:如果直接 del,可能会阻塞 Redis 服务。大致有下面几种处理办法:
如果 key 类型为 string,则直接删除;
如果 key 类型为 hash、list、set、sorted set,使用 hscan 命令,每次获取部分(例如100个)field-value,再利用 hdel 删除每个 field;
Redis 在4.0 版本支持 lazy delete free 的模式,删除 Bigkey 不会阻塞 Redis。
控制大小或拆分Bigkey
处理 Bigkey 的另外一种方法就是控制大小,比如 string 减少字符串长度,list、hash、set、zset 等减少成员数。
有时也可以考虑对 Bigkey 进行拆分,具体方法如下:
对于 string 类型的 Bigkey,可以考虑拆分成多个 key-value。
对于 hash 或者 list 类型,可以考虑拆分成多个 hash 或者 list。