Redis性能优化

本文为个人学习笔记整理,仅供交流参考,非专业教学资料,内容请自行甄别

文章目录


一、Bigkey问题

Bigkey指的是,在Redis中,某个key对应的value

  • 字符串类型的value:单个大小大于10kb
  • 非字符串的value:集合中的元素过多,或者单个元素的key过大

Bigkey可能会导致以下的问题:

  • 执行命令阻塞:因为Redis执行命令是单线程的,单个key对应的value过大,必然导致处理时间的增加,在处理该key时,其他的请求必须等待。
  • 网络拥塞:key的value过大,占用带宽过大,处理的请求数量也就越少。
  • 过期删除:如果大量的Bigkey同时到期,在删除时同样会面临执行命令阻塞的问题。

造成Bigkey的原因,主要是针对业务场景,Redis存储粒度设计不当,可能包含以下的情况:

  • 将包含商品表中所有字段的对象,序列化作为String结构的value。
  • 将所有用户的信息,作为Hash结构的value存储。

对于Bigkey的优化,可以从以下的方面入手:

  • 将key在业务的层面进行拆分,例如存储用户的信息,可以分key存储。
  • 进行存储时,仅仅存储业务上必要的字段数据。

如果无法避免使用Bigkey,在查询时不要使用getAll将所有的数据都取出,仅仅通过get查询需要的部分。并且给key设置不同的过期时间,防止集中过期。

二、命令使用

尽量不要使用查询所有的命令,例如hgetallsmemberskeys等。如果有需要进行遍历,推荐使用渐进式的命令,例如scan渐进式地遍历key,假设我先向Redis中存入10个key:

使用scan命令,这里的count不是代表要扫描多少个精确的值。下一次的cursor应该是本次查询出的结果,cursor为0时代表扫描结束,语法如下:

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

后续的扫描:

但是使用该命令可能存在查询重复的问题,需要用户在客户端自己进行防重复处理。

同时在线上环境,一些危险操作应该禁用,例如flushall,flushdb等全量删除操作,可以使用rename机制修改这些命令。

并且Redis的数据库功能,实际上对于并发提升没有帮助,因为默认的16个数据库都是存在同一个redis中的,用户访问0数据库的时候,对于1数据库的访问的请求实际是阻塞的。

对于批量操作,可以使用pipeline提高效率,但是一次pipeline发送的命令数量同样不应过多,pipeline是Redis非原生的批量处理命令,相当于数据库的批量查询,省去了每次单条查询的IO操作。(十次单条查询,十次IO,将十次单条查询合并成一次批量查询,只需要一次IO),Redis处理数据的时间,远远小于每次数据发送,建立连接的时间。但是该操作不具有原子性,无法保证同一批的操作要么同时成功,要么同时失败。它允许一批次的命令,部分执行成功,部分执行失败。最后会把所有的结果都返回,包括成功和失败的。

三、连接池的设置

对于连接的管理,Redis同样采用了和线程池类似的池化技术,其中有三个关键的参数,可以根据需要在客户端进行配置:

  • maxTotal:也称为maxActive,为整个连接池允许的最大连接数,不能超过redis的最大连接数(maxClient)10000,大于最大连接数的所有请求全部拒绝掉。客户端的个数 * maxTotal 不能大于maxClient。
  • maxIdle:连接池中允许最大空闲的连接数。
  • minIdle:连接池中确保最小空闲的连接数。

假设我当前连接池中有50个最大的连接:

  • minIdle设置为10,表示整个连接池中最少有10个空闲的连接常驻。
  • maxidle设置为20,表示整个连接池中最多有20个空闲的连接常驻。

高并发的场景下来了30个连接,在高并发时期过去之后,连接会释放到20(maxidle),这20个连接尽管没有使用,也会常驻在redis中。

默认的空闲连接数,选择的是maxIdle ,如果修改配置,那连接数会释放到10(minIdle),剩下的10个连接尽管没有使用,也会常驻在redis中。

maxidle和maxtotal设置的相同,则高并发场景下,每次拿连接都不用重新生成,连接不会频繁生成和销毁。

Redis的连接池是懒加载的,所以在面对系统刚启动,就有大量请求访问Redis的场景下,需要进行连接池预热:可以创建一些redis的连接,然后执行一些简单的命令。需要等到所有的命令执行完成后,再将连接全部归还到连接池。

java 复制代码
List<Jedis> minIdleJedisList = new ArrayList<>(jedisPoolConfig.getMinIdle());

// 第一步:提前获取 minIdle 个 Jedis 连接,触发连接池初始化
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
    Jedis jedis = null;
    try {
        jedis = pool.getResource();
        minIdleJedisList.add(jedis);
        jedis.ping(); // 触发连接
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    } finally {
        // 注意:此处不能立刻 close,否则连接立即归还,等于只初始化了一个连接
    }
}

// 第二步:统一归还连接到连接池
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
    Jedis jedis = null;
    try {
        jedis = minIdleJedisList.get(i);
        if (jedis != null) {
            jedis.close(); // 归还连接
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
}

四、Redis的缓存过期策略

Redis的缓存过期,主要可以分为:

  • 被动删除:给key设置过期时间的情况,设置了过期时间,到达过期时间后并不会立刻删除,而是需要用户再去访问一次才会删除(惰性删除)
  • 定期删除:定期寻找过期的key,直接删除,防止某个key一直没有被访问到,也就无法被删除。
  • 当前运用的内存超过限定时,触发主动清理策略
    • 设置了过期时间的key:LRU算法,淘汰最久没有使用的key。LFU算法,淘汰最近一段时间内访问最少的key,热点数据优先选用。
    • 针对所有的key:上述两种,加上一种随机删除策略。
    • 不处理,不会剔除任何数据,对于新的请求拒绝,并且只响应读取操作。

针对热点缓存的淘汰,不应该按照时间(LRU),应该按照次数(LFU)。因为假设某个热点key,在1分钟之前被访问了100次,某个冷门的key,在1个小时之前被访问了1次,然后在10s前又被访问了一次,如果按照LRU,那么被访问了100次的热点key反而会被淘汰。

在配置文件中可以设置内存上限触发淘汰策略的阈值 淘汰策略,默认的淘汰策略是noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息

补充:布隆过滤器

解决缓存三大问题中的缓存穿透,最为普遍的操作是缓存空值。但是上一篇中提到,如果针对不同的不存在value的key进行恶意攻击,那么redis中会存在很多空值,对于内存的占用也是可观的。而布隆过滤器是解决缓存穿透的另一种方案,大量的不存在的数据,缓存空值在redis中,占用的空间远远大于布隆过滤器。

开启布隆过滤器时,用户在set某个key时,除了向缓存中存,还要存到布隆过滤器中:进行多次hash运算(key的hash运算结果,对于bitmap数组的长度取模),每次运算出结果对应的位置,数组中对应下标的bit位从0改成1:

进行三次不同的Hash运算

在get key时,执行和set key相同的hash运算。假设多次hash运算,有一次每次运算出结果对应的位置bit位为0,证明该key不存在。

可以通过Redisson去实现布隆过滤器:

java 复制代码
public class RedissonBloomFilter {
    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        //构造Redisson
        RedissonClient redisson = Redisson.create(config);

        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
        //初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
        bloomFilter.tryInit(100000000L, 0.03);
        //将zhuge插入到布隆过滤器中
        bloomFilter.add("test1");

        //判断下面号码是否在布隆过滤器中
        System.out.println(bloomFilter.contains("test1"));//false
        System.out.println(bloomFilter.contains("test2"));//false
        System.out.println(bloomFilter.contains("test3"));//true
    }
}

总结

相关推荐
kikikidult26 分钟前
(2025.07)解决——ubuntu20.04系统开机黑屏,左上角光标闪烁
笔记·ubuntu
future141233 分钟前
C#每日学习日记
java·学习·c#
m0_6530313637 分钟前
腾讯云认证考试报名 - TDSQL数据库交付运维专家(TCCE MySQL版)
运维·数据库·腾讯云
power 雀儿41 分钟前
集群聊天服务器---MySQL数据库的建立
服务器·数据库·mysql
近津薪荼1 小时前
初学者关于数据在内存中的储存的笔记
笔记
骑着王八撵玉兔2 小时前
【性能优化与架构调优(二)】高性能数据库设计与优化
数据库·性能优化·架构
碎叶城李白3 小时前
若依学习笔记1-validated
java·笔记·学习·validated
im_AMBER3 小时前
学习日志05 python
python·学习
都叫我大帅哥3 小时前
🌊 Redis Stream深度探险:从秒杀系统到面试通关
java·redis
都叫我大帅哥3 小时前
Redis持久化全解析:从健忘症患者到记忆大师的逆袭
java·redis