Day(85)11黑马点评-总结

黑马点评

键值设计

优雅key结构

表名:列名:主键

项目名:业务名:数据名:id

image-20260119131935296

使用String时底层调用sds数据结构,当字节小于等于44时,使用embstr编码,是一个连续内存块,提高性能。

我们可以把redisObject(对象头)想象成一个快递盒里的“快递单”,然后包装SDS盒子里的“货物”。

1.两者的核心区别:内存布局
虽然都使用了 SDS,但 Redis 决定如何将“快递单”和“货物”装入内存的方式不同:

embstr(嵌入式SDS)
布局:redisObject并且SDS是紧密贴在一起的,存储在一个连续的内存区域中。

分配方式:只调用1次 malloc(需要内存分配)。

逻辑:就像把快递单直接贴在货物包装上一样,它们是一个整体关系。

原始(原始SDS)
布局:redisObject和SDS是分开的存储,中间通过一个指针连接。

分配方式:需要调用2次 malloc,一次给对象头,一次给SDS。

逻辑:就像快递单贴在一个袋子上,袋子里面通过一张纸条(拖鞋)告诉你货物关系在仓库的另一个角落。

2.为什么要用SDS?
无论采用哪种布局,Redis都需要SDS的特性来管理字符串:

O(1) 吸入长度:SDS 内部维护了len字段。

二进制安全:SDS不以\0判断结束,可以存入任何数据二进制。

防止枢纽溢出:SDS自动处理空间扩大容量。

  1. 统一都用 SDS,为什么还要分一样?
    这主要是一个性能平衡:

内存碎片优化:embstr只分配一次内存,对内存管理更友好,产生的碎片更少。

缓存命中率:由于embstr内存连续,CPU加载时能同时把对象头和数据加载缓存,读取速度更快。

修改成本:

embstr是修改修改的。如果你想一个embstr,Redis 没有任何对应的修改 API,必须先把它转换成raw再。

raw专门为墙壁修改设计。它有动漫的休闲空间(alloc- len),修改时不需要墙壁重新分配整块内存。

image-20260119132117724

image-20260119133112470

image-20260119133129931

image-20260119133147189

image-20260119133406734

image-20260119133451480

增量扫描

image-20260119134319261

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.100.128", 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();
        }
    }
}
第三方工具

如何删除BigKey

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

  • redis 3.0 及以下版本
    • 如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey

Redis 4.0以后

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

image-20260119140301003

image-20260119140420348

动态修改上限(重启失效)

image-20260119140540973

image-20260119140742174

image-20260119141243546

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

image-20260119141629120

批处理优化

大数据导入

image-20260119143310787

image-20260119143403851

image-20260119143451029

MSET批量插入

image-20260119143713125

@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;//0-999左移一位,代表乘以二;012345变成0246810
        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));
}

0-999左移一位,代表乘以二;012345变成0246810

mset要求key和value紧挨着

pipeline管道

image-20260119144626869

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

mset是内置操作,原子性执行完;pipeline只是一起传输给redis,并不是一起在redis里面一起执行

image-20260119145002265

集群模式下批处理

image-20260119145353434

并行slot

串行代码

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自带工具类multiSet(lettuce)底层异步执行

@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);

    }

持久化配置

重启Redis后,它去哪里读数据?
这是一个优先级问题。当Redis重启时,它会像下面这样“寻找”数据:
场景A:开启了AOF (appendonly yes)
Redis优先读取AOF文件。
• 理由:AOF 记录的数据通常比 RDB 更完整(AOF 是一秒一记,RDB 是几分一记)。为了保证数据安全,只要 AOF 存在,Redis 就会无视 RDB。
场景B:关闭了AOF,只打开了RDB
Redis 读取 RDB 文件。
• 它会寻找dir配置路径下的dbfilename(默认是dump.rdb),将其加载内存。
场景C:两者都打开
依然优先读取AOF。
• 即使 RDB 文件更新、更大,Redis 也选择 AOF,因为它认为 AOF 的数据丢失风险最低。
场景D:两者都关了
Redis启动后内存为空。
• 如果你没有配置任何持久化,Redis重启就像新电脑开机一样,干干净净。

开启bgrewrite和aof,bgrewrite执行完成将内存以rdb格式存在aof文件里面,然后后续的aof可能每秒执行一次将修改指令追加到这个aof文件的rdb格式后面

每一次BGREWRITEAOF都是一次“彻底的新生”,它会产生一个全新的文件来替代旧文件。
具体到你的疑问,可以从以下三个细节来理解:
1.它不是“删除”,而是“替换”
在技术实现上,Redis并不是先删除旧AOF文件的内容再往里写,因为如果重写过程中断电了,旧数据就丢了。
• 第一步:开启一个全新的Redis临时文件。
• 第二步:把当前内存里的数据以RDB格式(如果开启了混合模式)写入进去,完全不看旧AOF里有什么。
• 第三步:写完后,用这个新文件原子性地替换(重命名)掉旧的AOF文件。
• 结果:旧AOF里的所有格式化信息(比如你之前记的几万行流水账)都随着旧文件的消失而彻底消失了。
2.重写后的“新起点”
第二次BGREWRITEAOF执行完成后,AOF文件会变成这样:
1. 全新的RDB头部:包含了到第二次重写那一刻的所有内存快照。
2. 空的或极小的AOF尾部:只包含第二次重写期间产生的极少量增量指令。
之前的第一次重写留下的RDB信息和后来追加的AOF指令,全部被揉碎、合并到了这次新的RDB头部中。

image-20260119152531462

权衡性能和安全

image-20260119152738471

慢查询

image-20260119153305874

image-20260119153522502

image-20260119153648051

命令以及安全配置

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

image-20260119160332533

image-20260119160351694

/usr/local/src/redis-6.2.6

image-20260119161559603

内存配置

1.内存碎片是怎么产生的?(书架乱了)
    Redis在运行过程中,间隙地写入、修改和删除数据:
    间隙申请与释放:当你删除一个旧密钥并写入一个新密钥时,新数据的大小可能和旧数据不匹配。
    内存分配器(jemalloc)的机制:为了效率,内存分配器会预先划分好固定大小的格子(比如8KB、16KB)。如果你的数据是5KB,它就占了一个8KB的格子,剩下的3KB就闲置了,变成了“碎片”。
    无法收回的空白:久而久之,书架上到处都是零散的小空位,虽然总加起来很大,但当你想放一本“大书”时,却找不到连续的完整空间。
2.重启为什么能解决?(重新更换)
    当你重新启动 Redis 时,发生了以下核心动作:
    彻底释放:重启意味着Redis进程退出,空间会占用该进程占用的所有物理内存。这就好比清空了整个书架。
    数据重载:Redis从RDB或AOF文件中恢复数据。
    连续写入:在恢复数据时,Redis 是连续、集中向内存申请空间。内存分配器将数据紧凑地排列在一起。
    布局优化:新申请的内存块是连续的,知道那些散落的星星的“小气泡”全部消失了。
3.除了重启,还有其他办法吗?
    重启虽然见效最快,但会导致服务短暂不可用。如果你不想重启,可以使用Redis 4.0之后提供的自动清理功能:
    手动开启清理:
    config set activedefrag yes
    原理:Redis会在后台启动一个线程,扫描内存中的碎片,把数据从零散的地方搬运到连续的空间,然后释放旧的碎片空间。这就相当于电脑的“磁盘碎片整理”。

image-20260119163257459

image-20260119163404948

image-20260119164505512

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

1、设置一个大小

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

image-20260119164748916

集群的最佳实践

image-20260119164927590

重配之后重写启动集群

关闭集群

printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown

查看redis运行情况

ps -ef | grep redis

删除旧文件

rm -rf 7001 7002 7003 8001 8002 8003

创建目录文件夹

mkdir 7001 7002 7003 8001 8002 8003

复制配置文件

[root@localhost tmp]# echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf

修改每个目录下的redis.conf,将其中的6379修改为与所在目录一致:

printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i 's/6379/{}/g' {}/redis.conf

启动

printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf

查看状态

ps -ef | grep redis

创建集群

redis-cli --cluster create --cluster-replicas 1 192.168.100.128:7001 192.168.100.128:7002 192.168.100.128:7003 192.168.100.128:8001 192.168.100.128:8002 192.168.100.128:8003

查看集群状态

[root@localhost tmp]# redis-cli -p 7001 cluster nodes

关闭某一个redis

redis-cli -p 7001 shutdown

连接集群

redis-cli -p 7002 -c

image-20260119172251653

image-20260119172313791

image-20260119173130849

posted @ 2026-01-19 17:38  David大胃  阅读(0)  评论(0)    收藏  举报