Redis高级篇之最佳实践

news2025/1/13 17:27:22

Redis高级篇之最佳实践

本章内容

  • Redis 键值设计
  • 批处理优化
  • 服务端优化
  • 集群最佳实践

笔记整理自 b站_黑马程序员Redis入门到实战教程

1. Redis键值设计

优雅的key结构

Redis 的 Key 虽然可以自定义,但最好遵循下面的几个最佳实践约定:

  • 遵循基本格式:[业务名称]:[数据名]:[id]
  • 长度不超过 44 字节
  • 不包含特殊字符

例如:我们的登录业务,保存用户信息,其 key 可以设计成如下格式:

image-20221214204312778

这样设计的好处:

  • 可读性强
  • 避免 key 冲突
  • 方便管理
  • 更节省内存: key 是 string 类型,底层编码包含 int、embstr 和 raw 三种。embstr 在小于 44 字节使用,采用连续内存空间,内存占用更小。当字节数大于 44 字节时,会转为 raw 模式存储,在 raw 模式下,内存空间不是连续的,而是采用一个指针指向了另外一段内存空间,在这段空间里存储 SDS 内容,这样空间不连续,访问的时候性能也就会收到影响,还有可能产生内存碎片。

image-20221214212715997

拒绝BigKey

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

  • Key 本身的数据量过大:一个 String 类型的 Key,它的值为 5MB
  • Key 中的成员数过多:一个 ZSET 类型的 Key,它的成员数量为 10,000 个
  • Key 中成员的数据量过大:一个 Hash 类型的 Key,它的成员数量虽然只有 1,000 个但这些成员的 Value(值)总大小为 100 MB

那么如何判断元素的大小呢?redis 也给我们提供了命令:

image-20221214212719144

推荐值:

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

BigKey的危害

  • 网络阻塞
    • 对 BigKey 执行读请求时,少量的 QPS 就可能导致带宽使用率被占满,导致 Redis 实例,乃至所在物理机变慢
  • 数据倾斜
    • BigKey 所在的 Redis 实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
  • Redis 阻塞
    • 对元素较多的 hash、list、zset 等做运算会耗时较久,使主线程被阻塞
  • CPU 压力
    • 对 BigKey 的数据序列化和反序列化会导致 CPU 的使用率飙升,影响 Redis 实例和本机其它应用

如何发现BigKey

① redis-cli --bigkeys

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

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

image-20221215111434527

② scan扫描

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

image-20221215111431967

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();
        }
    }

}

image-20221215110256197

③ 第三方工具
  • 利用第三方工具,如 Redis-Rdb-Tools 分析 RDB 快照文件,全面分析内存使用情况。

    • 离线分析,对于 Redis 的性能没有任何的损耗,但这种离线分析会有一些时效上的差异,并不一定是最新的。
  • https://github.com/sripathikrishnan/redis-rdb-tools

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

image-20221215111426918

如何删除BigKey

BigKey 内存占用较多,即便时删除这样的 key 也需要耗费很长时间,导致 Redis 主线程阻塞,引发一系列问题。

  • Redis 3.0 及以下版本

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

    image-20220521140621204

  • Redis 4.0 以后

    • Redis 在 4.0 后提供了异步删除的命令:unlink

    image-20221215111206688

恰当的数据类型

例1:比如存储一个User对象,我们有三种存储方式:

① 方式一:json字符串
user:1{"name": "Jack", "age": 21}
  • 优点:实现简单粗暴
  • 缺点:数据耦合,不够灵活
② 方式二:字段打散
user:1:nameJack
user:1:age21
  • 优点:可以灵活访问对象任意字段
  • 缺点:占用空间大、没办法做统一控制
③ 方式三:hash(推荐)✅
user:1namejack
age21
  • 优点:底层使用 ziplist,空间占用小,可以灵活访问对象的任意字段
  • 缺点:代码相对复杂

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

keyfieldvalue
someKeyid:0value0
..........
id:999999value999999

存在的问题:

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

    image-20221215112140203

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

方案一

拆分为 string 类型

keyvalue
id:0value0
..........
id:999999value999999

存在的问题:

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

image-20221215114611799

  • 想要批量获取这些数据比较麻烦
方案二

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

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

image-20221215112329843

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();
        }
    }
}

总结

  • Key 的最佳实践
    • 固定格式:[业务名]:[数据名]:[id]
    • 足够简短:不超过 44 字节
    • 不包含特殊字符
  • Value 的最佳实践:
    • 合理的拆分数据,拒绝 BigKey
    • 选择合适数据结构
    • Hash 结构的 entry 数量不要超过 1000(默认是 500,如果达到上限则底层会使用哈希表而不是 ZipList,内存占用较多)
    • 设置合理的超时时间

2. 批处理优化

Pipeline

我们的客户端与redis服务器是这样交互的

  • 单个命令的执行流程

image-20221215120639998

  • N 条命令的执行流程

image-20221215120642337

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

image-20221215120644362

MSet

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

  • mset
  • hmset

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

@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) + "ms");
}

/*
 * 控制台打印
 * time: 182ms
 */

向 redis 中插入 10w 条数据,如果我们 for 循环执行 10w 次 redis 的插入指令的话,大概需要花费 44s 的时间:

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

而我们使用 mset 则仅需要 182ms,由此可见批处理的重要性。

Pipeline

MSET 虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用 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) + "ms");
}

/*
 * 控制台打印
 * time: 248ms
 */

使用 Pipeline 管道命令,比 mset 命令稍微慢了一点,但也在毫秒级别,可以忽略。

而对于何时使用 Pipeline 和 mset,则要看添加的数据类型,mset 的命令有一定的限制,而 Pipeline 是无限制的,它可以任意命令做组合,key 也可以自定义。

两者还有一个底层的区别:mset 操作为什么会比 Pipeline 要快?

  • 是因为 mset 操作是 redis 内置的操作,它会把这些插入指令作为一个原子性操作,一次性全执行完,中间不会有其他命令插队。

  • 而 Pipeline 则不是,Pipeline 在执行时,这一组命令会发送到 redis 中,但这一组命令不一定是一起执行的,你的命令在发送时,其它客户端也有可能向 redis 发送命令,所以 Pipeline 的执行时长可能会因为别的命令插队 从而比预期时间要长一些。

总结

  • 批量处理的方案:
    • 原生的 M 操作
    • Pipeline 批处理
  • 注意事项:
    • 批处理时不建议一次携带太多命令
    • Pipeline 的多个命令之间不具备原子性

集群下的批处理

如 MSET 或 Pipeline 这样的批处理需要在一次请求中携带多条命令,而此时如果 Redis 是一个集群,那批处理命令的多个 key 必须落在一个插槽中,否则就会导致执行失败。

大家可以想一想这样的要求其实很难实现,因为我们在批处理时,可能一次要插入很多条数据,这些数据很有可能不会都落在相同的节点上,这就会导致报错了。

image-20221215154646008

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

image-20221220153036766

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

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

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

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

    image-20221215155158010

串行化执行代码实践

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();
        }
    }
}

Spring集群环境下批处理代码(并行slot)

   @Test
    void testMSetInCluster() {
        Map<String, String> 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);
    }

原理分析

在 RedisAdvancedClusterAsyncCommandsImpl 类中

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

通过 RedisFuture<String> mset = super.mset(op); 进行异步的消息发送(第三种方案:并行 slot)。

@Override
public RedisFuture<String> mset(Map<K, V> map) {

    // 根据传入map中的key计算插槽,插槽一样的 则将这些key存入一个List中
    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<>();

    // 遍历,根据key拿到value
    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)));
		// mset操作,返回值是Future,说明是异步操作(也就是第三种方案:并行slot,最好的方案)
        RedisFuture<String> mset = super.mset(op);
        executions.put(entry.getKey(), mset);
    }

    return MultiNodeExecution.firstOfAsync(executions);
}

3. 服务器端优化-持久化配置

Redis 的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:

  • 用来做缓存的 Redis 实例尽量不要开启持久化功能
  • 建议关闭 RDB 持久化功能,使用 AOF 持久化
    • 设置合理的 rewrite 阈值,避免频繁的 bgrewrite
    • 配置 no-appendfsync-on-rewrite = yes,禁止在 rewrite 期间做 aof,避免因 AOF 引起的阻塞

image-20221215161404149

  • 利用脚本定期在 slave 节点做 RDB,实现数据备份
  • 部署有关建议:
    • Redis 实例的物理机要预留足够内存,应对 fork 和 rewrite
    • 单个 Redis 实例内存上限不要太大,例如 4G 或 8G。可以加快 fork 的速度、减少主从同步、数据迁移压力
    • 不要与 CPU 密集型应用部署在一起
    • 不要与高硬盘负载应用一起部署。例如:数据库、消息队列

4. 服务器端优化-慢查询优化

什么是慢查询

并不是很慢的查询才是慢查询,而是:在 Redis 执行时耗时超过某个阈值的命令,称为慢查询。

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

image-20221215120951644

  • 慢查询的阈值可以通过配置指定:
    • slowlog-log-slower-than:慢查询阈值,单位是微秒。默认是 10000,建议 1000。
  • 慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定:
    • slowlog-max-len:慢查询日志(本质是一个队列)的长度。默认是 128,建议 1000。

image-20221215121003527

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

image-20221215121005654

如何查看慢查询

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

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

image-20221215160725165

5. 服务器端优化-命令及安全配置

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

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
  • 尽量不是有默认的端口

6. 服务器端优化-Redis内存划分和内存配置

当 Redis 内存不足时,可能导致 Key 频繁被删除、响应时间变长、QPS 不稳定等问题。当内存使用率达到 90% 以上时就需要我们警惕,并快速定位到内存占用的原因。

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

有关碎片问题分析

  • Redis 底层分配并不是这个 key 有多大,他就会分配多大,而是有他自己的分配策略,比如 8,16,20 等等,假定当前 key 只需要 10 个字节,此时分配 8 肯定不够,那么他就会分配 16 个字节,多出来的 6 个字节就不能被使用,这就是我们常说的 碎片问题。
  • 但是碎片问题是不需要我们去考虑的,事实上,我们重启 redis 时,这些碎片就会被自动回收掉了,所以要想解决碎片问题,我们只需要定期的重启 redis 服务即可(当然我们需要按照主从、集群分批的重启)。

进程内存问题分析

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

缓冲区内存问题分析

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

于是我们就需要通过一些命令,可以查看到 Redis 目前的内存分配状态:

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

image-20221215160516947

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

image-20221215160531885

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

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

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

以上复制缓冲区和 AOF 缓冲区 不会有问题,最关键就是客户端缓冲区的问题。

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

image-20221216165938604

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

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

7. 服务器端集群优化-集群还是主从

集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:

  • 集群完整性问题
  • 集群带宽问题
  • 数据倾斜问题
  • 客户端性能问题
  • 命令的集群兼容性问题
  • lua 和事务问题

问题1:在 Redis 的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务:

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

image-20221217155919223


问题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 集群。
群都不能用了,我们在开发中,其实最重要的是可用性,所以需要把如下配置修改成 no,即有 slot 不能使用时,我们的 redis 集群还是可以对外提供服务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/110213.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Docker容器中安装Jenkins

众所周知,jenkins是现在比较流行的一种工具,今天就记录一下在工作中如何使用了jenkins&#xff0c; 由于我使用的使用Linux(Debain 11)开发环境使用了jdk1.8&#xff0c;会跟最新版的jenkins(官方介绍最新版要jdk11支持)有不良的化学反应&#xff0c;所以把jenkins放到了容器中…

Hive+Spark离线数仓工业项目实战--项目介绍及环境构建(2)

Docker的介绍 了解Docker的基本功能和设计 - 为什么要用Docker&#xff1f; - 什么是Docker&#xff1f; 路径 - step1&#xff1a;生产环境的问题 - step2&#xff1a;容器的概念 - step3&#xff1a;Docker的设计 实施 生产环境的问题 - 运维层面&#xff1a;一…

Windows下的通用进程守护程序(持续更新中),高仿supervisor。

&#x1f4e2;欢迎点赞 &#xff1a;&#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff0c;赐人玫瑰&#xff0c;手留余香&#xff01;&#x1f4e2;本文作者&#xff1a;由webmote 原创&#x1f4e2;作者格言&#xff1a;无尽的折腾后&#xff0c;终于又回到…

【Three.js入门】标准网格材质、置换贴图、粗糙度贴图、金属贴图、法线贴图

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;也会涉及到服务端 &#x1f4c3;个人状态&#xff1a; 在校大学生一枚&#xff0c;已拿多个前端 offer&#xff08;秋招&#xff09; &#x1f680;未…

【实时数仓】DWS层的定位、DWS层之访客主题计算(PV、UV、跳出次数、计入页面数、连续访问时长)

文章目录一 DWS层与DWM层的设计1 设计思路2 需求梳理3 DWS层定位二 DWS层-访客主题计算1 需求分析与思路2 功能实现&#xff08;1&#xff09;封装VisitorStatsApp&#xff0c;读取Kafka各个流数据a 代码b 测试&#xff08;2&#xff09;合并数据流a 封装主题宽表实体类Visitor…

CMakeList

目录 .1 简介 .2 常用命令 2.1 指定 cmake 的最小版本 2.2 设置项目名称 2.3 设置编译类型 2.4 指定编译包含的源文件 2.4.1 明确指定包含哪些源文件 2.4.2 搜索所有的 cpp 文件 2.4.3自定义搜索规则 2.5 查找指定的库文件 2.6. 设置包含的目录 2.7. 设置链接库搜索…

中国晶振市场规模将增长至2026年的263.21亿元,国产市场未来可期

晶振作为频率控制和频率选择基础元件&#xff0c;广泛应用于资讯设备、移动终端、通信及网络设备、汽车电子、智能电表、电子银行口令卡等领域&#xff0c;随着新兴电子产业、物联网的快速发展&#xff0c;及以 5G、蓝牙 5.0、Wi-Fi 6 等无线通信新技术的广泛应用&#xff0c;预…

基于Python的Flask WEB框架实现后台权限管理系统(含数据库),内容包含:用户管理、角色管理、资源管理和机构管理

#基于Flask实现后台权限管理系统 重磅&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 全新的风格界面&#xff0c;完全的前后端分离。基于ElementUI&#xff0c;前端代码基于RuoYi…

plotly parallel_coordinates平行坐标可视化

使用plotly画平行坐标图&#xff0c;代码如下&#xff1a; 其中数据使用excel的csv格式&#xff08;当然可以使用其它格式&#xff09;&#xff0c;csv的标头是参数名。 import plotly.express as px import numpy as np import pandas as pd# df px.data.iris() df pd.read…

【爬虫】JS逆向解决反爬问题系列3—sign破解

⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ 🐴作者:秋无之地 🐴简介:CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作,主要擅长领域有:爬虫、后端、大数据开发、数据分析等。 🐴欢迎小伙伴们点赞👍🏻、收藏⭐️、…

DOM(一):获取页面元素、操作元素

DOM&#xff08;一&#xff09;获取页面元素事件操作元素获取页面元素 1.根据ID获取 使用getElementById()方法获取带有ID的元素对象,格式如下&#xff1a; var 变量名 document.getElementById(‘id名’); 例如&#xff1a; <div id time>2022-12-18</div> &…

PyTorch——应用一个已训练好的图片分类网络——AlexNet

1.识别一个图像主体的预训练网络 ImageNet数据集是由一个Stanford大学维护的包含1400多万幅图像的非常大的数据集&#xff0c;所有的图像都用来自WordNet数据集的名词层次结构标记&#xff0c;而WordNet数据集又是一个大型的英语词汇数据库。 1.1获取一个预先训练好的网络用于…

【矩阵论】7.范数理论——基本概念——矩阵范数生成向量范数谱范不等式

7.1.3 矩阵范数产生向量范数 CnnC^{n\times n}Cnn 上任一矩阵范数 ∥∙∥\Vert \bullet\Vert∥∙∥ 都产生一个向量范数 φ(X)∥X∥V\varphi(X)\Vert X\Vert_Vφ(X)∥X∥V​ 矩阵范数与向量范数的相容性&#xff1a;φ(Ax)≤∥A∥φ(x)\varphi(Ax)\le \Vert A\Vert\varphi(x)φ…

蓝牙耳机什么牌子好?音质好、配置高的蓝牙耳机分享

​经常听到很多网友在讨论哪些蓝牙耳机好用&#xff0c;什么蓝牙耳机的配置高......选购蓝牙耳机无非就是音质、蓝牙技术、续航、佩戴体验等各方面条件&#xff0c;还有哪位朋友不知道该如何选购蓝牙耳机的&#xff1f;根据我对蓝牙耳机的了解&#xff0c;从网上整理了好几款蓝…

【关于eps8266自动重启 Soft WDT reset】

【关于eps8266自动重启 Soft WDT reset】1. 前言2. 分析问题2.1 长时间没有喂狗2.2 delayMicroseconds 函数触发3. 解决问题3.1 解决长时间没有喂狗3.2 解决delayMicroseconds 函数触发5. 小结1. 前言 最近使用esp8266进行远程遥控时, 但是在驱动舵机servo库的过程中出现了esp…

第二证券|两大板块掀涨停潮,有个股猛拉20cm!这只港股复牌一度暴跌

A股商场周五上午窄幅震动&#xff0c;上证指数微涨0.09点&#xff0c;核算机等板块领涨。 新股体现持续分解&#xff0c;4只今日上市的新股中&#xff0c;两只科创板新股上涨&#xff0c;两只北交所新股则破发。 港股全体小幅调整&#xff0c;全体动摇不算大&#xff0c;但仍…

【推荐】智慧农业解决方案资料合集30篇

智慧农业 是农业中的智慧经济&#xff0c;智慧经济形态在农业中的具体表现。智慧农业是智慧经济重要的组成部分&#xff1b;对于发展中国家而言&#xff0c;智慧农业是智慧经济主要的组成部分&#xff0c;是发展中国家消除贫困、实现后发优势、经济发展后来居上、实现赶超战略的…

16含风光水的虚拟电厂与配电公司协调调度模型(场景削减MATLAB程序)

参考文献 含风光水的虚拟电厂与配电公司协调调度模型——董文略&#xff08;复现场景削减部分&#xff09; 主要内容 代码主要做的是风电、光伏以及电价场景不确定性模拟&#xff0c;首先由一组确定性的方案&#xff0c;生成50种光伏场景&#xff0c;为了避免大规模光伏场景…

小结 | 支持向量机 (SVM)

一.基本原理 SVM是一种二分类模型 基本思想&#xff1a;在特征空间中寻找间隔最大的分离超平面使数据得到高效的二分类&#xff0c;具体来讲&#xff0c;有三种情况&#xff08;不加核函数的话就是线性模型&#xff0c;加了之后才会升级为一个非线性模型&#xff09; 当训练…

MybatisPlus详解 | DQLDML快速开发... | 系统性学习 | 无知的我费曼笔记

无知的我正在复盘MybatisPlus&#xff0c;顺便上传笔记。。。 下图是我总结的 MP 知识的初级思维导图&#xff0c;后续会不断补充 文章目录1 MyBatisPlus入门案例与简介1.1 入门案例回顾SpringBoot整合Mybatis的开发过程:步骤1:创建数据库及表步骤2:创建SpringBoot工程步骤…