当前位置: 首页 > article >正文

Redis-03高级篇下-最佳实践:

Redis高级篇之最佳实践

今日内容

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

1、Redis键值设计

1.1、优雅的key结构

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

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

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

在这里插入图片描述

这样设计的好处:

  • 可读性强:

    • 任何人只要了解这个格式,看到这个key就知道key的作用了,登录业务:存的用户信息:10号用户的
  • 避免key冲突:

    • 我们没有直接使用用户的id作为key,开发中如果不同的业务使用的是同一台redis,所有的业务都是存储在一起的,2个不同的业务都使用用户id作为key,这样key就用可能重复,数据产生覆盖。
  • 方便管理:

    • redis的key以冒号隔开时,在redis的客户端就会以层级结构显示,前缀一样的key就会出现在同一目录中,如果想要删除只需要删除最上层的目录即可,非常方便。
      在这里插入图片描述
  • 更节省内存: key是string类型,底层编码包含int(字符串的内容全是数值的时候使用int编码,把字符串当成数值去存储,这样占用空间更小)、embstr和raw三种。embstr在小于44字节使用,采用连续内存空间,内存占用更小。当字节数大于44字节时,会转为raw模式存储,在raw模式下,内存空间不是连续的,而是采用一个指针指向了另外一段内存空间,在这段空间里存储SDS内容,这样空间不连续,访问的时候性能也就会收到影响,还有可能产生内存碎片

测试:

  • object encoding key:查看key的底层编码
    在这里插入图片描述

1.2、拒绝BigKey

顾名思义BigKey就是占用内存很大的key(其实是和value有关)。

BigKey通常以Key的大小和Key中成员的数量来综合判定(如果没有成员只看大小即可),例如:

  • Key本身的数据量过大:一个String类型的Key,它的值为5 MB
    • String是单值的,最大存储为512M,达到5M就是一个非常大的k了。
  • Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个
    • 集合类型的key,比如list,set,成员数量达到1万个就会认为是BigKey。
  • Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB
    • 还有一些集合元素数量并不算多,但是每个成员都非常的大,最后合起来达到了100M,也会被称为BigKey。

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

  • STRLEN key:查看String类型的value的字节长度
  • LLEN key:查看list集合的长度
    在这里插入图片描述

推荐值:

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

1.2.1、BigKey的危害

  • 网络阻塞
    • 对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢
      • 什么是QPS? QPS即每秒查询率,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准
  • 数据倾斜
    • BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
  • Redis阻塞
    • 对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞
  • CPU压力
    • 对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用

1.2.2、如何发现BigKey

①redis-cli --bigkeys

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

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

注意:

  • 不要再redis的控制台运行,而是在黑窗口运行。

  • 得到的是每一种数据类型占用内存最多的第一名那个,排名第一的不一定是bigkey,即使第一个是bigkey那排名第二的也可能是bigkey,现在只显示排名第一的那不就把排名第二的bigkey错过了。

  • 这种方式的统计不够完整只能看到第一名,因此只做一个参考。

在这里插入图片描述

②scan扫描

自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度,以此来判断是不是BigKey(此处不建议使用MEMORY USAGE,因为它太消耗内存了)

注意:

  • 为什么这里不能使用keys * 扫描所有的key??
    • 因为keys * 是一次性把所有的key一次性都拿出来,数据多时这个命令可能会执行几十秒的时间,这么长的时间redis的主线程就会被阻塞,因此这个命令在生产环境下是不能使用的。
  • scan扫描不会占用redis的主线程,并且一次只扫描一部分。
    • scan 游标(第一次是0) 匹配那种类型的key(不指定就是扫描所有) 一次获取几个(默认是10个)类型
      在这里插入图片描述
    • scan 0:返回值第一个是下一次光标的位置
      在这里插入图片描述

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; //字符串的最大值:10kb
    final static int HASH_MAX_LEN = 500; //集合类型的key,元素数量最大为500,这里没有设置为1000,因为这里给的数据没有大的数据

    @Test //redis是主从结构的话,最好放在从节点执行
    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;
            }
            // 遍历:获取每一个key
            for (String key : list) {
                // 判断key的类型:因为不同类型获取的方式不一样
                String type = jedis.type(key);
                switch (type) {
                    case "string":
                        len = jedis.strlen(key); //获取的长度
                        maxLen = STR_MAX_LEN; //和最大的长度作比较,String是和STR_MAX_LEN比,集合是和HASH_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;
                }
                //获取的当前实际长度大于要求的最大值长度,说明就是BigKey
                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();
        }
    }

}
  • 运行测试方法:凡是符合的都会列举出来,信息更加完整。
    在这里插入图片描述
③第三方工具
  • 利用第三方工具,如 Redis-Rdb-Tools 分析RDB快照文件,全面分析内存使用情况
  • https://github.com/sripathikrishnan/redis-rdb-tools

好处:对redis无侵入

缺点:它是离线方式,所以时效性比较差。

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

在这里插入图片描述

1.2.3、如何删除BigKey

找到BigKey后该怎么办呢?

  • 首先:删除BigKey,但是不是说这部分数据不要了,而是把这份数据拆分后重新储存,这样就把BigKey打散了。
  • 其次:选择合适的数据类型,分别存储,这样BigKey就变为小的key了。

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

  • redis 3.0 及以下版本
    • 如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey
    • 用户和scan一样
    • scan是扫描redis所有的key
    • 扫描集合的方案:
      • 扫描hash:HSCAN
      • 扫描set:SSCAN
      • 扫描SortedSet:ZSCAN

在这里插入图片描述

  • Redis 4.0以后
    • Redis在4.0后提供了异步删除的命令:unlink key(异步的删除key)
      在这里插入图片描述

1.3、恰当的数据类型

说明:

  • 我们知道BigKey会存在各种各样的问题,给我们的系统带来巨大的负担,所以一旦出现BigKey就必须及时的发现它然后删除它。
  • 仅仅删除是不够的,还要找到BigKey产生的原因,往往是业务设计不合理导致的,所以我们要分析业务找到更加合适的数据类型把BigKey变为正常的key。

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

①方式一:json字符串
user:1{“name”: “Jack”, “age”: 21}

优点:实现简单粗暴

缺点:数据耦合,不够灵活

  • 想要修改其中的某个字段:只能是把整个json字符串重新覆盖。
②方式二:字段打散
user:1:nameJack
user:1:age21

优点:可以灵活访问对象任意字段

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

  • 原来一个key就能保存现在需要多个key,虽然从数据上看没有增多,但是每一次存储k-v结构在redis内部都是有很多原信息要保存的,key越多现在想要存储的原信息也越多。
  • 现在想要获取user的所有信息,只能是一条条的获取。
③方式三:hash(推荐)

v又是一个hash结构

user:1namejack
age21

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

缺点:代码相对复杂

  • 因为使用hash结构的时候,我们就需要把这个user对象转化为hashmap的形式去存储,转化的时候有可能会考虑到对象的数据类型的转换,而json因为相对比较成熟,对象与json的序列化和反序列化都有非常成熟的解决方案了,不用考虑太多数据类型的问题。
  • 不过代码在复杂只要有相应的工具类做转换,相信也不是什么问题。因此在存储对象时我们推荐还是使用hash类型的数据结构。

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

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

存在的问题:

  • 内存占用:
    • 上面学写过hash结构底层使用ziplist,空间占用小。但是当hash的entry数量超过500时,会使用哈希表而不是ZipList,内存占用较多
    • entry:一组field-value键值对。
    • 这里是100万条entry数据超过500用的是hash结构存储,占用内存为62.23M。
      在这里插入图片描述
  • 可以通过hash-max-ziplist-entries配置entry上限。但是如果entry过多就会导致BigKey问题
    在这里插入图片描述

    • 问题:这里占用内存不就是因为没有用ziplist存储吗,那么我们把redis底层对于entry的限制调高一点不就行了???
    • 答:虽然确实可以调高,但是把这个上线调到100万,我们上面学习过集合类型的元素超过1000会导致BigKey的问题。
方案一

拆分为string类型:不要什么key了,直接把field里面的每一个作为key就行了,反正id是自增的没有重复,确实解决了BigKey的问题,而是拆分成100万条数据了。

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

存在的问题:

  • string结构底层没有太多内存优化,内存占用较多,并且每一个key都有原信息这些无效的占用。

在这里插入图片描述

  • 想要批量获取这些数据比较麻烦,只能一个个的去获取。
方案二

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

解释:

  • 将 id / 100 作为key:id是0~999999的100万条数据,100万除以100为1万条数据,也就是说将来会有1万个key,把一个hash拆分成了一万个hash,100万条数据放到1万个hash里每个hash有100条数据,没有超过500,底层用的还是ziplist。
  • 将id % 100 作为field:
    • 0~99一共100条id,这些id除以100结果都是0作为key,这些id取模的结果恰好是0到99,刚好形成一个hash。
    • 100~199这些元素,除以100结果都是1作为key,取余是0到99。
keyfieldvalue
key:0id:00value0
..........
id:99value99
key:1id:00value100
..........
id:99value199
....
key:9999id:00value999900
..........
id:99value999999

在这里插入图片描述

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

//往1个key里面塞10万条数据,没有用100万条数据
    @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);
    }

//塞10万条k-v数据
    @Test
    void testBigString() {
        for (int i = 1; i <= 100000; i++) {
            jedis.set("test:str:key_" + i, "value_" + i);
        }
    }

 //将这10万条数据按照100个进行拆分,每100个做一次存储
    @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.4、总结

  • Key的最佳实践
    • 固定格式:[业务名]:[数据名]:[id]
    • 足够简短:不超过44字节
    • 不包含特殊字符
  • Value的最佳实践:
    • 合理的拆分数据,拒绝BigKey
    • 选择合适数据结构
    • Hash结构的entry数量不要超过1000
    • 设置合理的超时时间

2、批处理优化

说明

  • 之前数据量少的情况下,将商铺的地理坐标信息一股脑的从数据库中查出来,之后导入到redis中。
  • 如果数据量非常的多,就不能这么暴力的往redis中写了,如果处理的不恰当可能会带来一些巨大的危害。

2.1、Pipeline

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

单个命令的执行流程

在这里插入图片描述

N条命令的执行流程

在这里插入图片描述

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

在这里插入图片描述

2.1.2、MSet

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

  • mset:只能处理String
  • hmset:只能处理hash

利用mset批量插入10万条数据
在这里插入图片描述

@Test
void testMxx() {
//一个k-v分别是2个元素,所以2000长度的数组可以放1000个k-v键值对
//  为什么定义只有1000个这样的数组,每次往redis中set的时候就是set这1000个键值对
//  为什么不把这10万条数据一次性的放到数组里,一次性放到redis中???
// 答:虽然一次性多发送一些命令可以提高效率,但是一次性插入太多的数据可能会导致带宽
//    占满,导致网络阻塞,这个时候就不能传输了,反而变慢了。
    String[] arr = new String[2000];
    int j;
    long b = System.currentTimeMillis();
    for (int i = 1; i <= 100000; i++) {
    //2000长度的数组保存的是1000长度的键值对,i % 1000的范围为0~999,那么剩下的就填不了了???
    //  答:左移一位,0~999任意的数字左移一位代表的就是乘以2的意思,从原来的0 1 2 3 4 5------->变为0 2 4 6 8 10 ...1998,从奇数变为偶数
    //  也就是说现在j为1998(倒数第二个数) j+1为1999(倒数第一个元素),恰好符合规则。
        j = (i % 1000) << 1;
        arr[j] = "test:key_" + i; //k
        arr[j + 1] = "value_" + i; //v
        //如何知道什么时候填满呢?
        //  从1开始循环,每循环一次填一组,循环1000次刚好填满,循环1000次i就等于1000,
        //  每到1000的倍数就填满了,i等于1000或1000的倍数是它与1000求余的结果永远是0,
        //  0在乘以2还是0,所以当j等于0时一定是填满的时候。
        if (j == 0) {
            jedis.mset(arr);//利用jedis实现:数组里的元素紧挨着的2个就是k-v结构
        }
    }
    long e = System.currentTimeMillis();
    System.out.println("time: " + (e - b));
}

测试:

  • 没有用MSet之前:1分多钟
    在这里插入图片描述
  • 使用了MSet之后:390毫秒,非常快。
    在这里插入图片描述

2.1.3、Pipeline

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

@Test
void testPipeline() {
    // 创建管道
    Pipeline pipeline = jedis.pipelined();
    long b = System.currentTimeMillis();
    for (int i = 1; i <= 100000; i++) {
        // 放入命令到管道:除了执行set还可以执行各种各样的命令(redis的一切命令都可以做)
        pipeline.set("test:key_" + i, "value_" + i);
        if (i % 1000 == 0) {
            // 每放入1000条命令,批量执行
            pipeline.sync();
        }
    }
    long e = System.currentTimeMillis();
    System.out.println("time: " + (e - b));
}
  • 也是毫秒级别的,非常快,但余m相比稍慢。
  • 为什么m操作要比p操作要快,因为m操作是redis的内置操作,m操作里面的多组k-v操作,它会把他当做原子行操作,也就是说一次性全执行完中间不会有其它命令插队。
  • 而p操作这一组命令是一起发到redis,但这一组命令一起执行就不一定了,因为Pipeline里面命令的传输是有先后顺序的,而且你的命令在传递给redis的过程中,其它的客户端也可以向redis发送命令,这些命令到达redis的客户端是又先后顺序的,这些命令到达redis之后会先到达一个队列排队,然后redis的线程就可以一次执行这些命令,在这个1000条命令中如果其它客户端也发送命令的时候,那你的实际时长因为插队可能比预期的时间更长一些,但是也可以接受。
    在这里插入图片描述

说明:

  • 这里是用jedis做的,StringRedisTemplate也有批处理,只不过2者使用的对象不同,这里不在演示了。

在这里插入图片描述

2.2、集群下的批处理

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

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

在这里插入图片描述

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

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

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

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

2.2.1 串行化执行代码实践

手动实现串行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);
    }

//报错:说明jedisCluster这种默认没有帮我们解决集群下的批处理问题
    @Test
    void testMSet() {
        jedisCluster.mset("name", "Jack", "age", "21", "sex", "male");

    }

//串行slot方案
    @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();
        }
    }
}

2.2.2 Spring集群环境下批处理代码

集群配置:
在这里插入图片描述

   @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);//设置map集合到redis中


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

    }

测试:

  • 成功:说明spring提供的这个客户端stringRedisTemplate里面,已经解决了集群下的批处理问题了。
    在这里插入图片描述

原理分析

在RedisAdvancedClusterAsyncCommandsImpl 类中

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

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

@Override
public RedisFuture<String> mset(Map<K, V> 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);
}

说明:底层用的是异步的slot方案(性能最好的哪一种),所以说当大家要去实现集群批处理的时候,强烈建议使用spring提供的工具类,不要自己去做。

3、服务器端优化-持久化配置(从这开始----用到再重新听吧)

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

  • 用来做缓存的Redis实例尽量不要开启持久化功能
    • 缓存数据的目的是提高查询的效率,即便缓存临时消失了,我们再次查询也可以把缓存给查出来,这部分数据的安全性没有那么高,因此没有必要做持久化。
    • 所以在公司内部开发的时候如果经济实力允许的情况下,建议把redis缓存相关的所有业务放到一个单独的实例去做,这个实例就不要开启持久化功能了,而对一些安全性较高的业务,比如分布式锁 做库存等等,这些模块在放到单独的一个redis实例在去开启持久化。
  • 建议关闭RDB持久化功能,使用AOF持久化
  • 利用脚本定期在slave节点做RDB,实现数据备份
  • 设置合理的rewrite阈值,避免频繁的bgrewrite
  • 配置no-appendfsync-on-rewrite = yes,禁止在rewrite期间做aof,避免因AOF引起的阻塞
  • 部署有关建议:
    • Redis实例的物理机要预留足够内存,应对fork和rewrite
    • 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
    • 不要与CPU密集型应用部署在一起
    • 不要与高硬盘负载应用一起部署。例如:数据库、消息队列

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

4.1 什么是慢查询

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

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

在这里插入图片描述

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

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

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

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

在这里插入图片描述

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

在这里插入图片描述

4.2 如何查看慢查询

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

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

在这里插入图片描述

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底层分配并不是这个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的承受能力

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

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

  • 集群完整性问题
  • 集群带宽问题
  • 数据倾斜问题
  • 客户端性能问题
  • 命令的集群兼容性问题
  • 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集群

8、结束语

亲爱的小伙帮们辛苦啦,咱们有关redis的最佳实践到这里就讲解完毕了,期待小伙们学业有成~~~~


http://www.kler.cn/a/553640.html

相关文章:

  • Nginx下proxy_redirect的三种配置方式
  • scroll、offset、client三大家族和getBoundingClientRect方法
  • android 定制mtp连接外设的设备名称
  • 基于JavaSpringboot+Vue实现前后端分离房屋租赁系统
  • lua的local 变量和self 变量的理解理解后解决自己写的bug.
  • 【论文阅读】DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning
  • git-提交时间和作者时间的区别
  • 当使用vcpkg安装的qt5时,在VS调用出现libcrypto-*-x64.dll不是有效路径时
  • Docker如何切换文件目录安装——详细攻略!
  • IOS UITextField 无法隐藏键盘问题
  • 苹果新品今日发布,AI手机市场竞争加剧,近屿智能专注AI人才培养
  • 《深度学习》——ResNet网络
  • 无人机避障——感知篇(采用Livox-Mid360激光雷达获取点云数据显示)
  • 使用 Python 和 OpenCV 进行图像边缘检测:从基础到实战
  • 基于MATLAB的均匀面阵MUSIC算法DOA估计仿真
  • 基于SpringBoot+vue粮油商城小程序系统
  • 汇能感知的光谱相机/模块产品有哪些?
  • 【机器学习】K折交叉验证(K-Fold Cross-Validation)
  • 网工项目实践2.4 北京公司安全加固、服务需求分析及方案制定
  • linux内核数据结构之哈希表