⌛ Redis 进阶学习 线程+大量KEY
一、Redis的线程发展史
1.1何为Redis的单线程
Redis 的单线程是指在早期版本中,Redis 主要使用单个线程来处理大部分操作。
在单线程模式下,Redis 具有以下特点:
-
顺序执行:所有的操作按照顺序依次执行,不会出现并发执行的情况。
-
无锁竞争:由于只有一个线程,不存在多个线程之间的锁竞争问题。
-
简单高效:简化了线程管理和并发控制的复杂性。
然而,单线程也存在一些限制:
- 性能瓶颈:在处理大规模并发请求时,可能会成为性能瓶颈。
- 阻塞问题:某些耗时操作可能会阻塞整个线程,影响其他操作的响应性。
1.2 何为Redis的多线程
Redis 的多线程是指在 Redis 6.0 版本中引入的一种机制,它允许 Redis 在处理网络请求和数据存储时使用多个线程,从而提高系统的吞吐量和性能。
在 Redis 6.0 之前,Redis 主要使用单个线程来处理所有操作,包括网络 I/O、数据读写等。虽然 Redis 采用了高效的事件驱动模型和多路复用技术,但在处理大规模并发请求时,单线程仍然可能成为性能瓶颈。
为了解决这个问题,Redis 6.0 引入了多线程机制。具体来说,Redis 的多线程主要用于处理网络 I/O 操作,将网络事件分发给多个工作线程进行处理,处理完成后再将结果交还给主线程进行后续操作。这样可以充分利用多核 CPU 的优势,提高 Redis 的处理能力和响应速度。
需要注意的是,Redis 的多线程机制并不是完全的多线程,数据的读写操作仍然是由主线程来完成的,以避免多线程并发访问数据时可能出现的竞态条件和数据不一致性问题。此外,Redis 的多线程机制还需要在性能和并发性之间进行平衡,以确保系统的稳定性和可靠性。
1.3 Redis线程的发展阶段图
二、大量Key问题的探讨
2.1 MoreKey
场景模拟:Redis中有百万条以上数据
实现步骤
① linux下执行脚本,生成百万条命令
for((i=1;i<=100*10000;i++)); do echo "set k$i v$i" >> /java/redisTest.txt ;done;
② 将这个文件里面的命令执行导Redis中
cd /usr/local/redis/bin/
./redis-server /root/myredis/redis.conf
cat /java/redisTest.txt | /usr/local/redis/bin/redis-cli -h 127.0.0.1 -p 6379 --pipe
我们在使用keys */flushdb/flushall等危险命令的时候就会出现Redis卡顿影响其他业务,导致Redis宕机从而出现生产事故
事故举例
③优化处理
3.1 修改配置文件:禁用这些危险命令
rename-command keys ""
rename-command flushdb ""
rename-command flushall ""
重启Redis测试
3.2 使用Scan 命令
Redis 的 Scan 命令 用于迭代数据库中的数据库键。它是一个基于游标的迭代器,每次被调用之后,都会向用户返回一个新的游标,用户在下次迭代时需要使用这个新游标作为 Scan 命令的游标参数,以此来延续之前的迭代过程。
SCAN cursor [MATCH pattern] [COUNT count]
scan 0 match k* count 15
- Cursor-游标。
- pattern-匹配的模式。
- count-指定从数据集里返回多少元素,默认值为10
2.2 BigKey
字符串类型的 BigKey 是指键对应的字符串值所占用的内存空间较大,通常当一个值超过 10KB 时,就可以被认为是字符串类型的 BigKey。
① --bigkeys
./redis-cli --bigkeys
② memory usage
MEMORY USAGE 命令给出一个 key 和它的值在 RAM 中所占用的字节数。返回的结果是 key 的值以及为管理该分配的内存总字节数。对于嵌套数据类型,可以使用选项 SAMPLES,其中coumt表示抽样的元素个数,默认值为5。当需要抽样所有元素时,使用SAMPLES0
MEMORY USAGE key [SAMPLES count] :(计算每个键值的字节数)
③ 优化删除
首先创建Springboot项目、导入Redis
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
3.1 String
一般用del,如果过于庞大unlink
3.2 hash
使用hscan每次获取少量field-value,再使用hdel删除每个field
代码实现:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import redis.clients.jedis.Tuple;
import java.util.Set;
public class RedisBigKeyHashProcessor {
private static final String BIG_KEY_THRESHOLD = "1000"; // 设置BigKey的阈值
private static final int SCAN_COUNT = 100; // 每次扫描的Key数量
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379); // 连接到Redis服务器
try {
// 扫描所有Key
ScanParams scanParams = new ScanParams().count(SCAN_COUNT);
String cursor = "0";
do {
ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
cursor = scanResult.getCursor();
Set<String> keys = scanResult.getResult();
// 遍历每个Key,检查是否为Hash类型并且字段数量超过阈值
for (String key : keys) {
if (jedis.type(key).equals("hash") && jedis.hlen(key).toString().compareTo(BIG_KEY_THRESHOLD) > 0) {
handleBigKeyHash(jedis, key);
}
}
} while (!cursor.equals("0"));
} finally {
if (jedis != null) {
jedis.close();
}
}
}
private static void handleBigKeyHash(Jedis jedis, String key) {
// 处理BigKey Hash的逻辑,比如分割Hash等
long hashLength = jedis.hlen(key);
System.out.println("BigKey Hash detected: " + key + " with length: " + hashLength);
// 这里可以根据实际情况选择分割Hash或者进行其他处理
// 比如,可以使用hscan命令分批获取和处理hash字段
String cursor = "0";
ScanParams hashScanParams = new ScanParams().count(SCAN_COUNT);
do {
ScanResult<Tuple> hashScanResult = jedis.hscan(key, cursor, hashScanParams);
cursor = hashScanResult.getCursor();
Set<Tuple> fieldValues = hashScanResult.getResult();
// 处理每个字段
for (Tuple fieldValue : fieldValues) {
String field = fieldValue.getElement();
String value = fieldValue.getValue();
// 进行处理,比如迁移到新的Hash或删除不必要的字段
}
} while (!cursor.equals("0"));
}
}
使用了
hscan
命令来分批获取和处理Hash中的字段,而不是一次性加载整个Hash,这样可以避免在处理大型Hash时阻塞Redis服务器。
3.3 list
使用ltrim渐进式逐步删除,直到全部删除完成
代码实现:
import redis.clients.jedis.Jedis;
public class RedisBigKeyListProgressiveDeleter {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379); // 连接到Redis服务器
try {
String bigKey = "yourBigKey"; // 替换为你的BigKey
long listLength = jedis.llen(bigKey); // 获取List的长度
// 设置每次删除的步长,可以根据实际情况调整
int stepSize = 100;
// 使用LTRIM命令逐步删除List中的元素
long startIndex = 0;
while (startIndex < listLength) {
// 计算结束索引,确保不会删除太多元素导致性能问题
long endIndex = Math.min(startIndex + stepSize - 1, listLength - 1);
// 使用LTRIM命令截断List
jedis.ltrim(bigKey, startIndex, endIndex);
// 打印当前删除的进度
System.out.println("Deleted up to index " + endIndex + " of " + bigKey);
// 更新起始索引,继续下一次删除
startIndex = endIndex + 1;
// 可以添加适当的延迟,以控制删除速率,避免对Redis造成过大压力
Thread.sleep(100); // 暂停100毫秒
}
// 删除完成后,确认List已被清空
if (jedis.llen(bigKey) == 0) {
System.out.println("List " + bigKey + " has been fully deleted.");
// 可以选择删除这个Key,如果不再需要的话
// jedis.del(bigKey);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
首先获取了BigKey List的长度,然后使用一个循环来逐步删除元素。每次循环中,我们计算截断的起始索引和结束索引,并使用
LTRIM
命令来截断List。我们添加了一个适当的延迟来控制删除速率,以避免对Redis服务器造成过大的压力。循环继续进行,直到整个List被删除。请注意,这个示例中的
stepSize
和Thread.sleep(100)
可以根据你的实际需求和Redis服务器的性能进行调整。如果Redis服务器性能允许,你可以增大stepSize
或减小延迟时间,以加快删除速度。相反,如果服务器性能受限,你应该减小stepSize
或增加延迟时间,以减轻服务器的压力。此外,删除完成后,我们检查了List是否已被清空,并可以选择删除这个Key(如果不再需要的话)。在实际应用中,你可能需要根据自己的业务逻辑来决定是否删除Key。
3.4 set
使用sscan每次获取部分元素,再使用srem命令删除每个元素
代码实现:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import java.util.Set;
public class RedisBigKeySetProgressiveDeleter {
private static final int SCAN_COUNT = 100; // 每次扫描的元素数量
private static final String BIG_KEY = "yourBigKeySet"; // 替换为你的BigKey Set
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379); // 连接到Redis服务器
try {
// 使用SSCAN命令渐进式删除Set中的元素
String cursor = "0";
ScanParams scanParams = new ScanParams().count(SCAN_COUNT);
do {
ScanResult<String> scanResult = jedis.sscan(BIG_KEY, cursor, scanParams);
cursor = scanResult.getCursor();
Set<String> elements = scanResult.getResult();
// 对获取到的元素进行删除操作
if (elements != null && !elements.isEmpty()) {
jedis.srem(BIG_KEY, elements.toArray(new String[0]));
System.out.println("Deleted elements: " + elements);
}
// 如果返回的cursor等于"0",表示遍历完成
} while (!cursor.equals("0"));
// 检查Set是否为空,如果为空则可以选择删除Key
if (jedis.scard(BIG_KEY) == 0) {
System.out.println("Set " + BIG_KEY + " has been fully deleted.");
jedis.del(BIG_KEY);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
使用了
sscan
命令来每次获取Set中的一部分元素,并通过srem
命令来删除这些元素。我们设置了一个SCAN_COUNT
常量来控制每次扫描的元素数量,这个值可以根据实际情况进行调整。在do-while循环中,我们使用
sscan
命令获取一部分元素,并对这些元素调用srem
命令进行删除。每次循环都会更新游标cursor
,直到游标值变为"0",表示遍历完成。最后,我们检查Set是否为空(使用
scard
命令获取Set的元素数量),如果为空,则删除这个Key。
3.5 zset
使用zscan每次获取部分元素,再使用ZREMRANGEBYRANK命令删除每个元素
代码实现:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import redis.clients.jedis.Tuple;
import java.util.Set;
public class RedisBigZSetProgressiveDeleter {
private static final String BIG_ZSET_KEY = "yourBigZSetKey"; // 替换为你的BigKey ZSet
private static final int SCAN_COUNT = 100; // 每次扫描的元素数量
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379); // 连接到Redis服务器
try {
// 使用ZSCAN命令渐进式删除ZSet中的元素
String cursor = "0";
ScanParams scanParams = new ScanParams().count(SCAN_COUNT);
do {
ScanResult<Tuple> scanResult = jedis.zscan(BIG_ZSET_KEY, cursor, scanParams);
cursor = scanResult.getCursor();
Set<Tuple> tuples = scanResult.getResult();
// 如果没有更多元素,退出循环
if (tuples == null || tuples.isEmpty()) {
break;
}
// 获取当前扫描到的元素的起始和结束排名
long start = tuples.iterator().next().getScore();
long end = tuples.stream().mapToLong(Tuple::getScore).max().getAsLong();
// 使用ZREMRANGEBYRANK命令删除指定排名范围内的元素
long deletedCount = jedis.zremrangeByRank(BIG_ZSET_KEY, start, end);
System.out.println("Deleted " + deletedCount + " elements from " + BIG_ZSET_KEY);
} while (!cursor.equals("0")); // 当cursor为"0"时,表示遍历完成
// 检查ZSet是否为空,如果为空则删除Key
if (jedis.zcard(BIG_ZSET_KEY) == 0) {
System.out.println("ZSet " + BIG_ZSET_KEY + " has been fully deleted.");
jedis.del(BIG_ZSET_KEY);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
设置了一个循环,使用
zscan
命令每次获取一部分ZSet元素的排名和成员。接着,我们使用zremrangebyrank
命令删除这些元素。这个过程持续进行,直到所有元素都被删除。注意,
zremrangebyrank
命令根据元素的排名范围来删除元素,所以我们通过start
和end
变量来确定要删除的元素范围。此外,
SCAN_COUNT
变量控制每次zscan
命令返回的元素数量,可以根据实际需要调整这个值以平衡性能和删除速度。最后,我们检查ZSet是否为空,如果为空,则删除该Key。
④ 生产调优
修改配置类
三、总结
本文主要介绍了Redis的高级篇、Redis的线程问题、什么是Redis的单线程、什么是Redis的多线程、以及各自的优缺点、Redis线程的发展阶段图。还介绍了Redis大Key的两种问题以及如何优化。
转载自:https://juejin.cn/post/7361253974051684361