Redis应用笔记

一、 前言

夯实Redis基础以及生产环境应用优化的笔记~
在这里插入图片描述

二、 Redis八种常用数据类型

  • 5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
  • 3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap(位图)、Geospatial (地理位置)。

2.1 Redis的五种基本数据类型

Redis 5 种基本数据类型对应底层数据结构:

StringListHashSetZset
SDSLinkedList/ZipList/QuickListDict、ZipListDict、IntsetZipList、SkipList
缓存 Session、Token、图片地址、序列化后的对象、计数、分布式锁最新文章、最新动态、消息队列用户信息、商品信息、文章信息、购物车信息网站 UV 统计、文章点赞、动态点赞、共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)、抽奖系统、随机点名排行榜、优先级任务队列

2.2 Redis的三种特殊数据类型

Bitmap (位图)HyperLogLog(基数统计)Geospatial (地理位置)
用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)数据量巨大的计数场景:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计需要管理使用地理空间数据的场景:附近的人

三、发现Redis热Key方案

3.1 使用 Redis 自带的 --hotkeys 参数来查找

Redis 4.0.3 版本中新增 hotkeys 参数,该参数能够返回所有 key 的被访问次数。但会增加 Redis 实例的 CPU 和内存消耗(全局扫描),因此需要谨慎使用。

使用该方案的前提条件是 Redis Server 的 maxmemory-policy 参数设置为 LFU 算法,不否则会报错:

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.

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

3.2 使用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"

可以选择在合适的时机短暂执行 MONITOR 命令并将输出重定向至文件,在关闭 MONITOR 命令后通过对文件中请求进行归类分析即可找出这段时间中的 hotkey。

3.3 借助开源项目

京东零售的 hotkey 这个项目不光支持 hotkey 的发现,还支持 hotkey 的处理。
在这里插入图片描述

四、解决 hotkey

  • 读写分离:主节点处理写请求,从节点处理读请求。
  • 使用 Redis Cluster:将热点数据分散存储在多个 Redis 节点上。
  • 二级缓存:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。

五、Redis 内存碎片

内存碎片可简单理解为不可用的空闲内存。
例如:操作系统为任务分配 32 字节的连续内存空间,而存储数据实际只需要使用 24 字节内存空间,那这多余出来的 8 字节内存空间如果后续没办法再被分配存储其他数据的话,就可以被称为内存碎片。
Redis 内存碎片虽然不会影响 Redis 性能,但是会增加内存消耗。

5.1 为何会有Redis 内存碎片

1、Redis 存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。Redis官网表明:Redis 使用zmalloc方法(Redis自己实现的内存分配方法)进行内存分配的时候,除要分配 size 大小的内存之外,还会多分配 PREFIX_SIZE 大小的内存。
zmalloc 方法源码:

void *zmalloc(size_t size) {
   // 分配指定大小的内存
   void *ptr = malloc(size+PREFIX_SIZE);
   if (!ptr) zmalloc_oom_handler(size);
#ifdef HAVE_MALLOC_SIZE
   update_zmalloc_stat_alloc(zmalloc_size(ptr));
   return ptr;
#else
   *((size_t*)ptr) = size;
   update_zmalloc_stat_alloc(size+PREFIX_SIZE);
   return (char*)ptr+PREFIX_SIZE;
#endif
}

源码地址:https://github.com/antirez/redis-tools/blob/master/zmalloc.c
另外,Redis 可使用多种内存分配器来分配内存( libc、jemalloc、tcmalloc),默认使用 jemalloc,按照一系列固定的大小(8 字节、16 字节、32 字节……)来分配内存的。

当程序申请的内存大小最接近某个固定值时,jemalloc会给它分配相应大小的空间。
例如程序需要申请 17 字节的内存,jemalloc会直接给它分配 32 字节的内存,这样会导致有 15 字节内存的浪费。
实际jemalloc专门针对内存碎片问题做过优化,一般不会存在过度碎片化问题。

2、频繁修改Redis中的数据会产生内存碎片。
Redis官网表明:当Redis中的某个数据删除时,Redis通常不会轻易释放内存给操作系统。
在这里插入图片描述

文档地址:https://redis.io/topics/memory-optimization

5.2 查看Redis内存碎片信息

使用 info memory 命令即可查看 Redis 内存相关的信息。
Redis官方文档有详细的介绍:https://redis.io/commands/INFO 。
在这里插入图片描述
Redis 内存碎片率的计算公式:

mem_fragmentation_ratio= used_memory_rss / used_memory

内存碎片率 = 操作系统实际分配给Redis的物理内存空间大小 / Redis内存分配器为存储数据实际申请使用的内存空间大小

内存碎片率的值越大代表内存碎片率越严重,那多大的内存碎片率才需要清理呢?
通常认为 mem_fragmentation_ratio > 1.5 就需要清理内存碎片。mem_fragmentation_ratio > 1.5 意味着使用Redis存储实际大小2G的数据需要使用大于3G的内存。
快速查看内存碎片率命令:

> redis-cli -p 6379 info | grep mem_fragmentation_ratio

而碎片率小于 1 时:

  • Redis 内存碎片率低并非只跟 SWAP 有关,生产环境通常建议禁用 SWAP。

  • 复制积压缓冲区配置较大、业务数据量较小的情况下极容易造成碎片率 远低于 1,这是正常现象,无需优化或调整。

  • 通常将线上环境复制缓冲区的值 repl-backlog-size 设置的比较大,目的是防止主库频繁出现全量复制而影响性能。

  • 随着业务数据量增长,Redis 内存碎片率比值会逐渐趋于 1。

5.3 清理 Redis 内存碎片

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

config set activedefrag yes

具体清理时间需要通过两个参数控制:

# 内存碎片占用空间达到 500mb 的时候开始清理
config set active-defrag-ignore-bytes 500mb
# 内存碎片率大于 1.5 的时候开始清理
config set active-defrag-threshold-lower 50

通过 Redis 自动内存碎片清理机制可能会对 Redis 的性能产生影响,可通过两个参数来减少对 Redis 性能的影响:

# 内存碎片清理所占用 CPU 时间的比例不低于 20%
config set active-defrag-cycle-min 20
# 内存碎片清理所占用 CPU 时间的比例不高于 50%
config set active-defrag-cycle-max 50

另外,重启节点可以做到内存碎片重新整理。如果采用的是高可用架构的 Redis 集群的话,可将碎片率过高的主节点转换为从节点,以便进行安全重启。(我上月遇到过,手工清理效果不佳,最后是重启节点解决,还是重启大法好使~)

六、Redis缓存预热

  • 减少冷启动影响:当系统重启或新启动时,缓存是空的,这被称为冷启动。冷启动可能导致首次请求处理缓慢,因为数据需要从慢速存储(如数据库)检索。
  • 提高数据访问速度:通过预先加载常用数据到缓存中,可以确保数据快速可用,从而加快数据访问速度。
  • 平滑流量峰值:在流量高峰期之前预热缓存可以帮助系统更好地处理高流量,避免在流量激增时出现性能下降。
  • 保证数据的时效性:定期预热可以保证缓存中的数据是最新的,特别是对于高度依赖于实时数据的系统。
  • 减少对后端系统的压力:通过缓存预热,可减少对数据库或其他后端服务的直接查询,从而减轻负载。

6.1 预热工具

  • RedisBloom:RedisBloom 是 Redis 的一个模块,提供多个数据结构,包括布隆过滤器、计数器、和 TopK 数据结构等。其中,布隆过滤器可以用于 Redis 缓存预热,通过将预热数据添加到布隆过滤器中,可以快速判断一个键是否存在于缓存中
  • Redis Bulk loading:这是一个官方基于 Redis 协议批量写入数据的工具
  • Redis Desktop Manager:Redis Desktop Manager 是一个图形化的 Redis 客户端,可以用于管理 Redis 数据库和进行缓存预热。通过 Redis Desktop Manager,可以轻松地将预热数据批量导入到 Redis 缓存中。

6.2 启动预热

6.2.1 Spring Boot特定逻辑接口

Spring Boot用于在应用程序启动后执行特定逻辑的接口:CommandLineRunnerApplicationRunner。在SpringApplication的 run方法中其实就是 callRunners(context, applicationArguments); 的实现。

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<>();
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    AnnotationAwareOrderComparator.sort(runners);
    for (Object runner : new LinkedHashSet<>(runners)) {
        if (runner instanceof ApplicationRunner) {
            callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
            callRunner((CommandLineRunner) runner, args);
        }
    }
}

可以新建两个类来使用。
MyCommandLineRunner.java:

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class MyCommandLineRunner implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        // 在应用启动后执行缓存预热逻辑
        // ...
    }
}

MyApplicationRunner.java:

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class MyApplicationRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 在应用启动后执行缓存预热逻辑
        // ...
    }
}

6.2.2 使用 InitializingBean 接口

实现 InitializingBean 接口,并在 afterPropertiesSet 方法中执行缓存预热的逻辑。这样Spring 在初始化 Bean 时会调用 afterPropertiesSet 方法。

import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class CachePreloader implements InitializingBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        // 执行缓存预热逻辑
        // ...
    }
}

6.2.3 使用@PostConstruct 注解

使用 @PostConstruct 注解标注一个方法,该方法将在 Bean 的构造函数执行完毕后立即被调用。在这个方法中执行缓存预热的逻辑。

import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;

@Component
public class CachePreloader {

    @PostConstruct
    public void preloadCache() {
        // 执行缓存预热逻辑
        // ...
    }
}

6.2.4 定时任务预热

启动预热有个问题,就是一旦启动之后,如果需要预热新的数据,或者需要修改数据,就不支持,那在应用的运行过程中,可通过定时任务来实现缓存的更新预热。
通常依赖这种方式来确保缓存中的数据是最新的,避免因为业务数据的变化而导致缓存数据过时。

@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行
public void scheduledCachePreload() {
    // 执行缓存预热逻辑
    // ...
}

也可使用 xxl-job 等定时任务实现。

6.2.5 缓存器预热

有些缓存框架提供缓存加载器的机制,可在缓存中不存在数据时,自动调用加载器加载数据到缓存中。这样可以简化缓存预热的逻辑。而实际项目中也需要两级缓存增加效率,例如:我们项目中使用Caffeine实现:

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class MyCacheService {

    private final LoadingCache<String, String> cache;

    public MyCacheService() {
        this.cache = Caffeine.newBuilder()
                .refreshAfterWrite(1, TimeUnit.MINUTES)  // 配置自动刷新,1分钟刷新一次
                .build(key -> loadDataFromSource(key));  // 使用加载器加载数据
    }

    public String getValue(String key) {
        return cache.get(key);
    }

    private String loadDataFromSource(String key) {
        // 从数据源加载数据的逻辑
        // 这里只是一个示例,实际应用中可能是从数据库、外部服务等获取数据
        System.out.println("Loading data for key: " + key);
        return "Value for " + key;
    }
}
  • 使用Caffeine.newBuilder().refreshAfterWrite(1, TimeUnit.MINUTES)配置缓存的自动刷新机制,即每个缓存项在写入后的 1 分钟内,如果有读请求,Caffeine 会自动触发数据的刷新。

  • loadDataFromSource 方法是用于加载数据的自定义方法。可在此方法中实现从数据源(例如数据库、外部服务)加载数据的逻辑。

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

容若只如初见

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值