Redis最佳实践

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis键值设计优雅的key结构Redis的Key虽然可以自定义,但最好遵循下面的几个最佳实践约定:遵循基本格式:[业务名称]:[数据名]:[id]长度不超过44字节不包含特殊字符例如:我们的登录业务,保存用户信息,其key可以设计成如下格式:这样设计的好处:可读性强避免key冲突方便管理更节省内存: key是string类型,(value)底层编码包含int、embstr和raw三种。key是数值的情况下,使用int编码,把字符串直接当作一个数字去存储,较小存储空间embstr在小于44字节使用,采用连续内存空间,内存占用更小。当字节数大于44

Redis键值设计
优雅的key结构
Redis的Key虽然可以自定义,但最好遵循下面的几个最佳实践约定:

遵循基本格式:[业务名称]:[数据名]:[id]
长度不超过44字节
不包含特殊字符
例如:我们的登录业务,保存用户信息,其key可以设计成如下格式:

这样设计的好处:

可读性强

避免key冲突

方便管理

更节省内存: key是string类型,(value)底层编码包含int、embstr和raw三种。

key是数值的情况下,使用int编码,把字符串直接当作一个数字去存储,较小存储空间
embstr在小于44字节使用,采用连续内存空间,内存占用更小。
当字节数大于44字节时,会转为raw模式存储,在raw模式下,内存空间不是连续的,而是采用一个指针指向了另外一段内存空间,在这段空间里存储SDS内容,这样空间不连续,访问的时候性能也就会收到影响,还有可能产生内存碎片

拒绝BigKey
BigKey 的具体表现是 redis 中的 key 对应的 value 很大,占用的 redis 空间比较大,本质上是大 value 问题。

BigKey通常以Key的大小和Key中成员的数量来综合判定,例如:

Key本身的数据量过大:一个String类型的Key,它的值为5 MB
Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个
Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB
那么如何判断元素的大小呢?redis也给我们提供了命令

推荐值:

单个key的value小于10KB
对于集合类型的key,建议元素数量小于1000
BigKey的危害

网络阻塞
对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢
QPS是 queries per second 的缩写,意思是“每秒查询次数”。它表示对Redis执行读取操作(如GET命令)的频率。
数据倾斜
BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
Redis阻塞
对元素较多的hash、list、zset等做运算会耗时较久,使主线程被阻塞
CPU压力
对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用
如何发现BigKey
①redis-cli --bigkeys

利用redis-cli提供的–bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key

命令:redis-cli -a 密码 --bigkeys

②scan扫描

自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE)

scan 命令调用完后每次会返回2个元素,第一个是下一次迭代的光标,第一次光标会设置为0,当最后一次scan 返回的光标等于0时,表示整个scan遍历结束了,第二个返回的是List,一个匹配的key的数组

import com.heima.jedis.util.JedisConnectionFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanResult;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JedisTest {
private Jedis jedis;

@BeforeEach
void setUp() {
    // 1.建立连接
    // jedis = new Jedis("192.168.150.101", 6379);
    jedis = JedisConnectionFactory.getJedis();
    // 2.设置密码
    jedis.auth("123321");
    // 3.选择库
    jedis.select(0);
}

final static int STR_MAX_LEN = 10 * 1024;
final static int HASH_MAX_LEN = 500;

@Test
void testScan() {
    int maxLen = 0;
    long len = 0;

    String cursor = "0";
    do {
        // 扫描并获取一部分key
        ScanResult<String> result = jedis.scan(cursor);
        // 记录cursor
        cursor = result.getCursor();
        List<String> list = result.getResult();
        if (list == null || list.isEmpty()) {
            break;
        }
        // 遍历
        for (String key : list) {
            // 判断key的类型
            String type = jedis.type(key);
            switch (type) {
                case "string":
                    len = jedis.strlen(key);
                    maxLen = STR_MAX_LEN;
                    break;
                case "hash":
                    len = jedis.hlen(key);
                    maxLen = HASH_MAX_LEN;
                    break;
                case "list":
                    len = jedis.llen(key);
                    maxLen = HASH_MAX_LEN;
                    break;
                case "set":
                    len = jedis.scard(key);
                    maxLen = HASH_MAX_LEN;
                    break;
                case "zset":
                    len = jedis.zcard(key);
                    maxLen = HASH_MAX_LEN;
                    break;
                default:
                    break;
            }
            if (len >= maxLen) {
                System.out.printf("Found big key : %s, type: %s, length or size: %d %n", key, type, len);
            }
        }
    } while (!cursor.equals("0"));
}

@AfterEach
void tearDown() {
    if (jedis != null) {
        jedis.close();
    }
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
③第三方工具

利用第三方工具,如 Redis-Rdb-Tools 分析RDB快照文件,全面分析内存使用情况
https://github.com/sripathikrishnan/redis-rdb-tools
④网络监控

自定义工具,监控进出Redis的网络数据,超出预警值时主动告警
一般阿里云搭建的云服务器就有相关监控页面

BigKey处理方式
bigkey 的常见处理以及优化办法如下(这些方法可以配合起来使用):

分割 bigkey:将一个 bigkey 分割为多个小 key。这种方式需要修改业务层的代码,一般不推荐这样做。
手动清理:Redis 4.0+ 可以使用 UNLINK 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 SCAN 命令结合 DEL 命令来分批次删除。
采用合适的数据结构:比如使用 HyperLogLog 统计页面 UV。
开启 lazy-free(惰性删除/延迟释放) :lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
如何删除BigKey
BigKey内存占用较多,即便是删除这样的key也需要耗费很长时间,导致Redis主线程阻塞,引发一系列问题。

redis 3.0 及以下版本

如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey

如何遍历呢?我们前面提到的scan是遍历redis中的所有的key,这里redis也给我们提供了以下方法来遍历集合

Redis 4.0以后

Redis在4.0后提供了异步删除的命令:unlink
恰当的数据类型
例1:比如存储一个User对象,我们有三种存储方式:

①方式一:json字符串

user:1 {“name”: “Jack”, “age”: 21}
优点:实现简单粗暴

缺点:数据耦合,不够灵活(不好修改单一属性值)

②方式二:字段打散

user:1:name Jack
user:1:age 21
优点:可以灵活访问对象任意字段

缺点:占用空间大、没办法做统一控制

③方式三:hash(推荐)

user:1 name jack
age 21
优点:底层使用ziplist,空间占用小,可以灵活访问对象的任意字段

缺点:代码相对复杂

例2:假如有hash类型的key,其中有100万对field和value,field是自增id,这个key存在什么问题?如何优化?

key field value
someKey id:0 value0
..... .....
id:999999 value999999
存在的问题:

hash的entry数量超过500时,会使用哈希表而不是ZipList,内存占用较多

可以通过hash-max-ziplist-entries配置entry上限。但是如果entry过多就会导致BigKey问题

方案一

拆分为string类型

key value
id:0 value0
..... .....
id:999999 value999999
存在的问题:

string结构底层没有太多内存优化,内存占用较多

想要批量获取这些数据比较麻烦

方案二

拆分为小的hash,将 id / 100 作为key, 将id % 100 作为field,这样每100个元素为一个Hash

key field value
key:0 id:00 value0
..... .....
id:99 value99
key:1 id:00 value100
..... .....
id:99 value199
....
key:9999 id:00 value999900
..... .....
id:99 value999999

package com.heima.test;

import com.heima.jedis.util.JedisConnectionFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.ScanResult;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JedisTest {
private Jedis jedis;

@BeforeEach
void setUp() {
    // 1.建立连接
    // jedis = new Jedis("192.168.150.101", 6379);
    jedis = JedisConnectionFactory.getJedis();
    // 2.设置密码
    jedis.auth("123321");
    // 3.选择库
    jedis.select(0);
}

@Test
void testSetBigKey() {
    Map<String, String> map = new HashMap<>();
    for (int i = 1; i <= 650; i++) {
        map.put("hello_" + i, "world!");
    }
    jedis.hmset("m2", map);
}

@Test
void testBigHash() {
    Map<String, String> map = new HashMap<>();
    for (int i = 1; i <= 100000; i++) {
        map.put("key_" + i, "value_" + i);
    }
    jedis.hmset("test:big:hash", map);
}

@Test
void testBigString() {
    for (int i = 1; i <= 100000; i++) {
        jedis.set("test:str:key_" + i, "value_" + i);
    }
}

@Test
void testSmallHash() {
    int hashSize = 100;
    Map<String, String> map = new HashMap<>(hashSize);
    for (int i = 1; i <= 100000; i++) {
        int k = (i - 1) / hashSize;
        int v = i % hashSize;
        map.put("key_" + v, "value_" + v);
        if (v == 0) {
            jedis.hmset("test:small:hash_" + k, map);
        }
    }
}

@AfterEach
void tearDown() {
    if (jedis != null) {
        jedis.close();
    }
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
总结

Key的最佳实践
固定格式:[业务名]:[数据名]:[id]
足够简短:不超过44字节
不包含特殊字符
Value的最佳实践:
合理的拆分数据,拒绝BigKey
选择合适数据结构
Hash结构的entry数量不要超过1000
设置合理的超时时间
处理HotKey
简单来说,如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey。例如在 Redis 实例的每秒处理请求达到 5000 次,而其中某个 key 的每秒访问量就高达 2000 次,那这个 key 就可以看作是 hotkey。

hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。

hotkey 有什么危害?

处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。

因此,hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性。

如何发现 HotKey?
方法一:使用 Redis 自带的 --hotkeys 参数来查找。

Redis 4.0.3 版本中新增了 hotkeys 参数,该参数能够返回所有 key 的被访问次数。

使用该方案的前提条件是 Redis Server 的 maxmemory-policy 参数设置为 LFU 算法,不然就会出现如下所示的错误。

redis-cli -p 6379 --hotkeys

Scanning the entire keyspace to find hot keys as well as

average sizes per key type. You can use -i 0.1 to sleep 0.1 sec

per 100 SCAN commands (not usually needed).

Error: ERR An LFU maxmemory policy is not selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust.
1
2
3
4
5
6
7
Redis 中有两种 LFU 算法:

volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。
allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。
以下是配置文件 redis.conf 中的示例:

使用 volatile-lfu 策略

maxmemory-policy volatile-lfu

或者使用 allkeys-lfu 策略

maxmemory-policy allkeys-lfu
1
2
3
4
5
需要注意的是,hotkeys 参数命令也会增加 Redis 实例的 CPU 和内存消耗(全局扫描),因此需要谨慎使用。

方法二:使用MONITOR 命令

MONITOR 命令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。

由于该命令对 Redis 性能的影响比较大,因此禁止长时间开启 MONITOR(生产环境中建议谨慎使用该命令)。

redis-cli

127.0.0.1:6379> MONITOR
OK
1683638260.637378 [0 172.17.0.1:61516] "ping"
1683638267.144236 [0 172.17.0.1:61518] "smembers" "mySet"
1683638268.941863 [0 172.17.0.1:61518] "smembers" "mySet"
1683638269.551671 [0 172.17.0.1:61518] "smembers" "mySet"
1683638270.646256 [0 172.17.0.1:61516] "ping"
1683638270.849551 [0 172.17.0.1:61518] "smembers" "mySet"
1683638271.926945 [0 172.17.0.1:61518] "smembers" "mySet"
1683638274.276599 [0 172.17.0.1:61518] "smembers" "mySet2"
1683638276.327234 [0 172.17.0.1:61518] "smembers" "mySet"
1
2
3
4
5
6
7
8
9
10
11
12
在发生紧急情况时,我们可以选择在合适的时机短暂执行 MONITOR 命令并将输出重定向至文件,在关闭 MONITOR 命令后通过对文件中请求进行归类分析即可找出这段时间中的 hotkey。

方法三:借助第三方开源项目

京东零售的 hotkey 这个项目不光支持 hotkey 的发现,还支持 hotkey 的处理。

方法四:根据业务情况提前预估

可以根据业务情况来预估一些 hotkey,比如参与秒杀活动的商品数据等。不过,我们无法预估所有 hotkey 的出现,比如突发的热点新闻事件等。

方法五:业务代码中记录分析

在业务代码中添加相应的逻辑对 key 的访问情况进行记录分析。不过,这种方式会让业务代码的复杂性增加,一般也不会采用。

方法六:借助公有云的 Redis 分析服务
如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现

文档地址:https://www.alibabacloud.com/help/zh/apsaradb-for-redis/latest/use-the-real-time-key-statistics-featureopen in new window 。

HotKey处理方式
hotkey 的常见处理以及优化办法如下(这些方法可以配合起来使用):

读写分离:主节点处理写请求,从节点处理读请求。
使用 Redis Cluster:将热点数据分散存储在多个 Redis 节点上。
二级缓存:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。
除了这些方法之外,如果你使用的公有云的 Redis 服务话,还可以留意其提供的开箱即用的解决方案。

这里以阿里云 Redis 为例说明,它支持通过代理查询缓存功能(Proxy Query Cache)优化热点 Key 问题。

批处理优化
我们的客户端与redis服务器是这样交互的
单个命令的执行流程

N条命令的执行流程

redis处理指令是很快的,主要花费的时候在于网络传输。于是乎很容易想到将多条指令批量的传输给redis

MSet
Redis提供了很多Mxxx这样的命令,可以实现批量插入数据,例如:

mset

hmset

利用mset批量插入10万条数据

这里我们使用的jedis(Java客户端库)来实现mset,我们可以传入一个数组,数组中紧挨着的两个元素就是key和value。

@Test
void testMxx() {
String[] arr = new String[2000];
int j;
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
j = (i % 1000) << 1;
arr[j] = "test:key" + i;
arr[j + 1] = "value
" + i;
if (j == 0) {
jedis.mset(arr);
}
}
long e = System.currentTimeMillis();
System.out.println("time: " + (e - b));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MSET会在Redis中作为一个事务执行,要么全部成功,要么全部失败。这保证了多个键值对要么同时存在,要么同时不存在。

Pipeline
MSET虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要(比如我要导入结构不同的key),建议使用Pipeline

@Test
void testPipeline() {
// 创建管道
Pipeline pipeline = jedis.pipelined();
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
// 放入命令到管道
pipeline.set("test:key" + i, "value" + i);
if (i % 1000 == 0) {
// 每放入1000条命令,批量执行
pipeline.sync();
}
}
long e = System.currentTimeMillis();
System.out.println("time: " + (e - b));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pipeline中还有hset、lpush、sadd、zadd等操作各种数据结构的命令。

集群下的批处理
在Redis Cluster中,如果一个命令需要操作多个key,那么这些key必须属于同一个hash slot,否则该命令会失败。
这是因为Redis Cluster的key空间被划分为16384个slot,每个slot对应一组key,并由一个主节点负责。所以,如果一个命令操作的多个key分布在不同的slot上,由不同的主节点负责,Redis Cluster无法确定应该在哪个节点上执行这个命令。
举例来说,如果有命令:
MGET key1 key2 key3
如果这三个key属于同一个slot,那么该命令会被转发到对应slot的主节点执行。
但是,如果key1属于slot1,key2属于slot2,key3属于slot3,那么这个MGET命令不知道应该去哪个节点获取value,无法执行,会返回一个错误。

所以,在Redis Cluster中,对多个key进行操作的命令(如MGET、MSET、ZINTERSTORE等)要求这些key必须映射到同一个slot,否则该命令会失败。

严格来说,要在Redis Cluster中执行多键命令,正确的要求应该是:多个key必须属于同一个节点的主从结构。如果多个key恰巧映射到同一个主节点,那么即使在不同插槽,命令也可以成功执行。但是如果映射到不同主节点,命令必定失败。当然如果映射到同一插槽那么肯定能成功执行。

如MSET或Pipeline这样的批处理需要在一次请求中携带多条命令,而此时如果Redis是一个集群,那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败。大家可以想一想这样的要求其实很难实现,因为我们在批处理时,可能一次要插入很多条数据,这些数据很有可能不会都落在相同的节点上,这就会导致报错了

这个时候,我们可以找到4种解决方案

第一种方案:串行执行,所以这种方式没有什么意义,当然,执行起来就很简单了,缺点就是耗时过久。

第二种方案:串行slot,简单来说,就是执行前,客户端先计算一下对应的key的slot,一样slot的key就放到一个组里边,不同的,就放到不同的组里边,然后对每个组执行pipeline的批处理,他就能串行执行各个组的命令,这种做法比第一种方法耗时要少,但是缺点呢,相对来说复杂一点,所以这种方案还需要优化一下

第三种方案:并行slot,相较于第二种方案,在分组完成后串行执行,第三种方案,就变成了并行执行各个命令,所以他的耗时就非常短,但是实现呢,也更加复杂。

第四种方案:hash_tag,redis计算key的slot的时候,其实是根据key的有效部分来计算的,通过这种方式就能一次处理所有的key,这种方式耗时最短,实现也简单,但是如果通过操作key的有效部分,那么就会导致所有的key都落在一个节点上,产生数据倾斜的问题,所以我们推荐使用第三种方式。

数据倾斜指的是大量的数据集中存放在少数的节点上,而其他节点的数据量很小。这会造成系统的性能瓶颈和故障风险增加。

串行化执行代码实践
public class JedisClusterTest {

private JedisCluster jedisCluster;

@BeforeEach
void setUp() {
    // 配置连接池
    JedisPoolConfig poolConfig = new JedisPoolConfig();
    poolConfig.setMaxTotal(8);
    poolConfig.setMaxIdle(8);
    poolConfig.setMinIdle(0);
    poolConfig.setMaxWaitMillis(1000);
    HashSet<HostAndPort> nodes = new HashSet<>();
    nodes.add(new HostAndPort("192.168.150.101", 7001));
    nodes.add(new HostAndPort("192.168.150.101", 7002));
    nodes.add(new HostAndPort("192.168.150.101", 7003));
    nodes.add(new HostAndPort("192.168.150.101", 8001));
    nodes.add(new HostAndPort("192.168.150.101", 8002));
    nodes.add(new HostAndPort("192.168.150.101", 8003));
    jedisCluster = new JedisCluster(nodes, poolConfig);
}

@Test
void testMSet() {
    jedisCluster.mset("name", "Jack", "age", "21", "sex", "male");

}

@Test
void testMSet2() {
    Map<String, String> map = new HashMap<>(3);
    map.put("name", "Jack");
    map.put("age", "21");
    map.put("sex", "Male");
    //对Map数据进行分组。根据相同的slot放在一个分组
    //key就是slot,value就是一个组
    Map<Integer, List<Map.Entry<String, String>>> result = map.entrySet()
            .stream()
            .collect(Collectors.groupingBy(
                    entry -> ClusterSlotHashUtil.calculateSlot(entry.getKey()))
            );
    //串行的去执行mset的逻辑
    for (List<Map.Entry<String, String>> list : result.values()) {
        String[] arr = new String[list.size() * 2];
        int j = 0;
        for (int i = 0; i < list.size(); i++) {
            j = i<<2;
            Map.Entry<String, String> e = list.get(0);
            arr[j] = e.getKey();
            arr[j + 1] = e.getValue();
        }
        jedisCluster.mset(arr);
    }
}

@AfterEach
void tearDown() {
    if (jedisCluster != null) {
        jedisCluster.close();
    }
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
从 Spring集群环境下批处理代码

@Test
void testMSetInCluster() {
Map map = new HashMap<>(3);
map.put("name", "Rose");
map.put("age", "21");
map.put("sex", "Female");
stringRedisTemplate.opsForValue().multiSet(map);

    List<String> strings = stringRedisTemplate.opsForValue().multiGet(Arrays.asList("name", "age", "sex"));
    strings.forEach(System.out::println);

}

1
2
3
4
5
6
7
8
9
10
11
12
13
原理分析

在RedisAdvancedClusterAsyncCommandsImpl 类中

首先根据slotHash算出来一个partitioned的map,map中的key就是slot,而他的value就是对应的对应相同slot的key对应的数据

通过 RedisFuture mset = super.mset(op);进行异步的消息发送

@Override
public RedisFuture mset(Map map) {

Map<Integer, List<K>> partitioned = SlotHash.partition(codec, map.keySet());

if (partitioned.size() < 2) {
    return super.mset(map);
}

Map<Integer, RedisFuture<String>> executions = new HashMap<>();

for (Map.Entry<Integer, List<K>> entry : partitioned.entrySet()) {

    Map<K, V> op = new HashMap<>();
    entry.getValue().forEach(k -> op.put(k, map.get(k)));

    RedisFuture<String> mset = super.mset(op);
    executions.put(entry.getKey(), mset);
}

return MultiNodeExecution.firstOfAsync(executions);

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
服务器端优化-持久化配置
Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:

用来做缓存的Redis实例尽量不要开启持久化功能
建议关闭RDB持久化功能,使用AOF持久化
利用脚本定期在slave节点做RDB,实现数据备份
设置合理的rewrite阈值,避免频繁的bgrewrite
配置no-appendfsync-on-rewrite = yes,禁止在rewrite期间做aof,避免因AOF引起的阻塞
部署有关建议:
Redis实例的物理机要预留足够内存,应对fork和rewrite
单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
不要与CPU密集型应用部署在一起
不要与高硬盘负载应用一起部署。例如:数据库、消息队列
服务器端优化-慢查询优化
什么是慢查询
并不是很慢的查询才是慢查询,而是:在Redis执行时耗时超过某个阈值的命令,称为慢查询。

慢查询的危害:由于Redis是单线程的,所以当客户端发出指令后,他们都会进入到redis底层的queue来执行,如果此时有一些慢查询的数据,就会导致大量请求阻塞,从而引起报错,所以我们需要解决慢查询问题。

慢查询的阈值可以通过配置指定:

slowlog-log-slower-than:慢查询阈值,单位是微秒。默认是10000,建议1000

慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定:

slowlog-max-len:慢查询日志(本质是一个队列)的长度。默认是128,建议1000

修改这两个配置可以使用:config set命令:

如何查看慢查询
知道了以上内容之后,那么咱们如何去查看慢查询日志列表呢:

slowlog len:查询慢查询日志长度
slowlog get [n]:读取n条慢查询日志
slowlog reset:清空慢查询列表

服务器端优化-命令及安全配置
安全可以说是服务器端一个非常重要的话题,如果安全出现了问题,那么一旦这个漏洞被一些坏人知道了之后,并且进行攻击,那么这就会给咱们的系统带来很多的损失,所以我们这节课就来解决这个问题。

Redis会绑定在0.0.0.0:6379,这样将会将Redis服务暴露到公网上,而Redis如果没有做身份认证,会出现严重的安全漏洞.
漏洞重现方式:https://cloud.tencent.com/developer/article/1039000

为什么会出现不需要密码也能够登录呢,主要是Redis考虑到每次登录都比较麻烦,所以Redis就有一种ssh免秘钥登录的方式,生成一对公钥和私钥,私钥放在本地,公钥放在redis端,当我们登录时服务器,再登录时候,他会去解析公钥和私钥,如果没有问题,则不需要利用redis的登录也能访问,这种做法本身也很常见,但是这里有一个前提,前提就是公钥必须保存在服务器上,才行,但是Redis的漏洞在于在不登录的情况下,也能把秘钥送到Linux服务器,从而产生漏洞

漏洞出现的核心的原因有以下几点:

Redis未设置密码
利用了Redis的config set命令动态修改Redis配置
使用了Root账号权限启动Redis
所以:如何解决呢?我们可以采用如下几种方案

为了避免这样的漏洞,这里给出一些建议:

Redis一定要设置密码
禁止线上使用下面命令:keys、flushall、flushdb、config set等命令。可以利用rename-command禁用。
bind:限制网卡,禁止外网网卡访问
开启防火墙
不要使用Root账户启动Redis
尽量不是有默认的端口
服务器端优化-Redis内存划分和内存配置
当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因。

有关碎片问题分析:

Redis底层分配并不是这个key有多大,他就会分配多大,而是有他自己的分配策略,比如8,16,20等等,假定当前key只需要10个字节,此时分配8肯定不够,那么他就会分配16个字节,多出来的6个字节就不能被使用,这就是我们常说的 碎片问题

进程内存问题分析:

这片内存,通常我们都可以忽略不计

缓冲区内存问题分析:

一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,所以这片内存也是我们需要重点分析的内存问题。

内存占用 说明
数据内存 是Redis最主要的部分,存储Redis的键值信息。主要问题是BigKey问题、内存碎片问题
进程内存 Redis主进程本身运⾏肯定需要占⽤内存,如代码、常量池等等;这部分内存⼤约⼏兆,在⼤多数⽣产环境中与Redis数据占⽤的内存相⽐可以忽略。
缓冲区内存 一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出。
于是我们就需要通过一些命令,可以查看到Redis目前的内存分配状态:

info memory:查看内存分配的情况

memory xxx:查看key的主要占用情况

接下来我们看到了这些配置,最关键的缓存区内存如何定位和解决呢?

内存缓冲区常见的有三种:

复制缓冲区:主从复制的repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过replbacklog-size来设置,默认1mb
AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行rewrite的缓冲区。无法设置容量上限
客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设置
以上复制缓冲区和AOF缓冲区 不会有问题,最关键就是客户端缓冲区的问题

客户端缓冲区:指的就是我们发送命令时,客户端用来缓存命令的一个缓冲区,也就是我们向redis输入数据的输入端缓冲区和redis向客户端返回数据的响应缓存区,输入缓冲区最大1G且不能设置,所以这一块我们根本不用担心,如果超过了这个空间,redis会直接断开,因为本来此时此刻就代表着redis处理不过来了,我们需要担心的就是输出端缓冲区

我们在使用redis过程中,处理大量的big value,那么会导致我们的输出结果过多,如果输出缓存区过大,会导致redis直接断开,而默认配置的情况下, 其实他是没有大小的,这就比较坑了,内存可能一下子被占满,会直接导致咱们的redis断开,所以解决方案有两个

1、设置一个大小

2、增加我们带宽的大小,避免我们出现大量数据从而直接超过了redis的承受能力

如何清理 Redis 内存碎片?
Redis4.0-RC3 版本以后自带了内存整理,可以避免内存碎片率过大的问题。直接通过 config set 命令将 activedefrag 配置项设置为 yes 即可。

config set activedefrag yes
1
具体什么时候清理需要通过下面两个参数控制:

内存碎片占用空间达到 500mb 的时候开始清理

config set active-defrag-ignore-bytes 500mb

内存碎片率大于 1.5 的时候开始清理

config set active-defrag-threshold-lower 50
1
2
3
4
通过 Redis 自动内存碎片清理机制可能会对 Redis 的性能产生影响,我们可以通过下面两个参数来减少对 Redis 性能的影响:

内存碎片清理所占用 CPU 时间的比例不低于 20%

config set active-defrag-cycle-min 20

内存碎片清理所占用 CPU 时间的比例不高于 50%

config set active-defrag-cycle-max 50
1
2
3
4
另外,重启节点可以做到内存碎片重新整理。如果你采用的是高可用架构的 Redis 集群的话,你可以将碎片率过高的主节点转换为从节点,以便进行安全重启。

服务器端集群优化-集群还是主从
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:

集群完整性问题
集群带宽问题
数据倾斜问题
客户端性能问题
命令的集群兼容性问题
lua和事务问题
问题1、在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务:

大家可以设想一下,如果有几个slot不能使用,那么此时整个集群都不能用了,我们在开发中,其实最重要的是可用性,所以需要把如下配置修改成no,即有slot不能使用时,我们的redis集群还是可以对外提供服务

问题2、集群带宽问题

集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:

插槽信息
集群状态信息
集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高,这样会导致集群中大量的带宽都会被ping信息所占用,这是一个非常可怕的问题,所以我们需要去解决这样的问题

解决途径:

避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
避免在单个物理机中运行太多Redis实例
配置合适的cluster-node-timeout值
问题3、命令的集群兼容性问题

有关这个问题咱们已经探讨过了,当我们使用批处理的命令时,redis要求我们的key必须落在相同的slot上,然后大量的key同时操作时,是无法完成的,所以客户端必须要对这样的数据进行处理,这些方案我们之前已经探讨过了,所以不再这个地方赘述了。

问题4、lua和事务的问题

lua和事务都是要保证原子性问题,如果你的key不在一个节点,那么是无法保证lua的执行和事务的特性的,所以在集群模式是没有办法执行lua和事务的

那我们到底是集群还是主从

单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,所以如果不是在万不得已的情况下,尽量不搭建Redis集群
————————————————
版权声明:本文为CSDN博主「十八岁讨厌编程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zyb18507175502/article/details/130572611

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
4天前
|
缓存 NoSQL 安全
Redis 最佳实践 [后端必看]
Redis 最佳实践 [后端必看]
19 0
|
1月前
|
存储 缓存 Java
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
|
1天前
|
NoSQL API Redis
最佳实践|如何使用c++开发redis module
本文将试着总结Tair用c++开发redis module中遇到的一些问题并沉淀为最佳实践,希望对redis module的使用者和开发者带来一些帮助(部分最佳实践也适用于c和其他语言)。
|
2月前
|
缓存 监控 NoSQL
|
3月前
|
消息中间件 存储 NoSQL
Redis开发最佳实践
Redis开发最佳实践
63 0
|
8月前
|
存储 缓存 NoSQL
Redis缓存应用与最佳实践:优化性能与处理挑战
本篇深入探讨了Redis在缓存应用中的最佳实践,旨在优化性能并处理常见的缓存挑战。我们首先介绍了设计高效缓存架构的基本原则,展示了如何使用Redis作为缓存存储来提升应用性能。进一步地,我们讨论了缓存更新策略,演示了如何在源数据更新时同时更新缓存,以确保数据的一致性。
383 0
|
6月前
|
缓存 NoSQL 安全
【后端必看】Redis 最佳实践
【后端必看】Redis 最佳实践
|
10月前
|
存储 监控 NoSQL
Redis哨兵的工作原理、配置和使用方法,以及相应的最佳实践
Redis哨兵的工作原理、配置和使用方法,以及相应的最佳实践
252 0
|
存储 运维 监控
Redis最佳实践:7个维度+43条使用规范,带你彻底玩转Redis | 附实践清单
Redis最佳实践:7个维度+43条使用规范,带你彻底玩转Redis | 附实践清单
|
运维 NoSQL Redis
《Redis最佳实践与实战指南》电子版地址
本书由七天玩转Redis实训营课程内容整理而成,不仅系统性地介绍Redis的整体架构及在多种场景下的最佳实践经验,而且揭秘阿里云Redis开发规范和运维解法,更有基于Redis的开发实操教程。
96 0
《Redis最佳实践与实战指南》电子版地址