目 录CONTENT

文章目录

Redis 多线程和 BigKey

Sakura
2023-12-24 / 0 评论 / 0 点赞 / 12 阅读 / 11679 字 / 正在检测是否收录...

Redis 单线程和 Bigkey

1. Redis 单线程和多线程

首先 Redis 在 3.0 的单线程时代,依然性能很快的原因:

  1. 基于内存操作:Redis 的所有数据都存在内存中,因此所有的运算都是内存级别的,所以性能好

  2. 数据结构简单:Redis 的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分都是 O(1)

  3. 多路复用和非阻塞 I/O :Redis使用I/O多路复用功能来监听多个socket连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了I/O阻塞操作

  4. 避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生

1.1 单线程

主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,Redis 在处理客户端的请求时包括获取 ( socket 读)、解析、执行、内容返回 ( socket 写 ) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是 Redis 对外提供键值存储服务的主要流程。

但Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等等,其实是由额外的线程执行的。Redis命令工作线程是单线程的,但是,整个Redis来说,是多线程的;

Redis 之前就一直用单线程的原因:

  1. 使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试;

  2. 即使使用单线程模型也并发的处理多客户端的请求,主要使用的是 IO 多路复用和非阻塞 IO

  3. 对于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 的单线程足够使用了

  1. 设置io-thread-do-reads配置项为 yes ,表示启动多线程。

  2. 设置线程个数。关于线程数的设置,官方的建议是如果为 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 *

SCAN | Redis

中文: 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 危害

  1. 内存不均,集群迁移困难

  2. 超时删除,其他的 key 瞬间就能删除,BigKey 删除可能会超时

  3. 网络流量阻塞,传输的时候大了

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

0

评论区