Redis 单线程和 Bigkey
1. Redis 单线程和多线程
首先 Redis 在 3.0 的单线程时代,依然性能很快的原因:
基于内存操作:Redis 的所有数据都存在内存中,因此所有的运算都是内存级别的,所以性能好
数据结构简单:Redis 的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分都是 O(1)
多路复用和非阻塞 I/O :Redis使用I/O多路复用功能来监听多个socket连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了I/O阻塞操作
避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生
1.1 单线程
主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,Redis 在处理客户端的请求时包括获取 ( socket 读)、解析、执行、内容返回 ( socket 写 ) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是 Redis 对外提供键值存储服务的主要流程。
但Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等等,其实是由额外的线程执行的。Redis命令工作线程是单线程的,但是,整个Redis来说,是多线程的;
Redis 之前就一直用单线程的原因:
使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试;
即使使用单线程模型也并发的处理多客户端的请求,主要使用的是 IO 多路复用和非阻塞 IO;
对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而并非 CPU。
1.2 为什么之后支持多线程了
正常情况下,可以用 del 命令删除 key , 但是当 key 是一个很大的对象时,例如 key 包含了成千上万个元素的 hash 集合,那么 del 命令就会造成 redis 主线程卡顿
这就是 BigKey 删除的问题
解决方法:使用惰性删除可以有效的避免 redis 卡顿的问题
// 异步的删除 kye,不会阻塞redis主线程
unlink key
// 擦除数据库
flushdb async
// 擦除所有
flushall async
把删除工作交给了子线程了进行处理了
1.3 Redis 6/7 的多线程特性
首先要明确一点
对于 Redis 的性能瓶颈主要在于内存和网络带宽,而非 CPU
随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度
为了应对这个问题:
采用多个 IO 线程来处理网络请求,提高网络请求处理的并行度,Redis 6/7 就是采用的这种方法。
Redis 的工作线程是单线程的,但是整个 Redis 来说是多线程的
1.4 Redis7 开启多线程
在 Redis6.0 及 7 后,多线程机制默认是关闭的,如果需要使用多线程功能,需要在redis.conf中完成两个设置
对于 80% 的公司来说,redis 的单线程足够使用了
设置
io-thread-do-reads
配置项为 yes ,表示启动多线程。设置线程个数。关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。
// 开启线程
io-thread-do-reads yes
// 线程数
io-thread 3
2. BigKey
2.1 MoreKey
向 redis 中插入 100w 条数据
# shellh脚本
for((i=1;i<=100*10000;i++)); do echo "set k$i v$i" >> /tmp/redisTest.txt ;done;
# 进入 redis 之后
# 使用 pipe命令
# 博客链接 https://www.sakurasss.top/archives/redisguan-dao-he-fa-bu-ding-yue#heading-2
cat /tmp/redisTest.txt | redis-cli -a redis_password --pipe
可以看到 100W 数据以及成功写入 redis
可以尝试使用 keys * 命令进行遍历
如果是虚拟机的话会快一些,但是如果是云服务器的话,100W条的 kye 遍历需要很久
所以,生产中限制keys*/flushdb/flushall等危险命令以防止误删误用?
# 在redis.conf中 security 下面有说明
rename-commandkeys ""
rename-commandflushdb ""
rename-commandFLUSHALL ""
2.2 使用 scan 代替 keys *
中文: https://redis.com.cn/commands/scan.html
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
# cursor - 游标。
# pattern - 匹配的模式。
# count - 指定从数据集里返回多少元素,默认值为 10 。
# SCAN 用于遍历当前数据库中的键。
# SSCAN 用于遍历集合键中的元素。
# HSCAN 用于遍历哈希键中的键值对。
# ZSCAN 用于遍历有序集合中的元素(包括元素成员和元素分值)
基于游标的迭代器,需要基于上一次游标延续之前达到迭代过程
以 0 作为游标开始一次新的迭代,知道命令返回游标 0 完成一次遍历
不保证每次执行都返回某个给定数量的元素,支持模糊查询
一次返回的数量不可控,只能是大概率符合 count 参数
第一个元素是用于下一次迭代的新游标
第二个元素是一个数据包含了所有被迭代的元素
SCAN的遍历顺序非常特别,它不是从第一维数组的第零位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。
2.3 BigKey 规范
多大算 BigKey
《阿里云 Redis 开发规范》
string 类型控制在 10KB 以内,hash、list、set、zset 元素个数不要超过 5000
BigKey 危害
内存不均,集群迁移困难
超时删除,其他的 key 瞬间就能删除,BigKey 删除可能会超时
网络流量阻塞,传输的时候大了
2.4 发现 BigKey
redis-cli --bigkeys
redis-cli -a password --bigkyes
给出每种数据结构Top 1 bigkey,同时给出每种数据类型的键值个数+平均大小
缺点: 无法查询大于 10kb 的所有 key,需要用到 memory usage
memory usage
查看某个 key 占的大小
memory usege key [SAMPLES count]
# SAMLES 查看嵌套的元素的,抽样元素个数
2.5 如何删除 BigKey
《阿里云 Redis 规范》
非字符串的bigkey,不要使用 del 删除使用 hscan、sscan、zscan方式渐进式删除,同时要注意防止 bigkey 过期时间自动删除问题(例如一个 200 万的 zset 设置1小时过期,会触发 del 操作,造成阻塞,而且该操作不会出现在慢查询中 ( latency 可查)
渐进式删除:
# 假设现在有如下的key HSET customer:001 id 11 cname sakura age 23 score 100 # 渐进式删除:先删除里面的全部字段/几个字段 HDEL customer:001 score age HDEL customer:001 id cname # 将字段渐进式删除之后,再使用del del customer:001
String
一般用 del,如果过于庞大 unlink
hash
hcan + hdel
: 使用 hscan每次获取少量 field-value , 再使用 hdel 删除每个 field
func delBigHash(host string, port int, password string, bighashKey string) {
// 建立连接
client := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", host, port),
Password: password,
})
// 游标从 0 开始
cursor := "0"
for {
scanResult, err := client.HScan(bighashKey, cursor, &redis.ScanParams{Count: 100}).Result()
if err != nil {
fmt.Println("Error:", err)
break
}
entryList := scanResult.Val
if entryList == nil || len(entryList) == 0 {
break
}
for _, entry := range entryList {
// 渐进式删除字段
err := client.HDel(bighashKey, entry).Err()
if err != nil {
fmt.Println("Error:", err)
}
}
cursor = scanResult.Cursor
}
// 最后删除BigKey
err := client.Del(bighashKey).Err()
if err != nil {
fmt.Println("Error:", err)
}
}
list
使用ltrime
对一个列表进行修剪,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被别除。
# 定义一个列表key
RPUSH list 1 2 3 4 5
# 使用ltrime修建
ltrime list 0 2 # 0 1 2 在区间内,会被留下
func delBigList(host string, port int, password string, bigListKey string) {
client := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", host, port),
Password: password,
})
llen, err := client.LLen(bigListKey).Result()
if err != nil {
fmt.Println("Error getting length of big list:", err)
return
}
counter := 0
left := -100
for counter < llen {
//每次从左侧截掉100个
_, err = client.LTrim(bigListKey, left, llen).Result()
if err != nil {
fmt.Println("Error trimming big list:", err)
return
}
counter += left
}
//最终删除key
_, err = client.Del(bigListKey).Result()
if err != nil {
fmt.Println("Error deleting big list:", err)
}
}
set
使用sscan
每次获取部分元素,再使用srem
命令删除每个元素
zset
使用zscan
每次获取部分元素,再使用ZREMRANGEBYRANK
命令删除每个元素
2.6 BigKey 调优
Redis 默认删除是阻塞删除,可以在 reids.conf 中设置 Lazyfreeing 惰性删除
lazyfree- lazy-server-del yes
lazyfree- lazy- flush yes
lazyfree- lazy- user-del yes
评论区