Redis 作为业界主流的内存数据库,凭借极致的读写性能被广泛应用于缓存、计数、会话存储、消息队列等场景。但绝大多数开发者对 Redis 的使用仅停留在 SET/GET 层面,线上频繁出现接口超时、内存 OOM、数据库被打穿、数据不一致等问题,根源在于没有从底层理解 Redis 的运行逻辑,调优仅停留在盲目堆砌参数。
一、缓存策略设计与调优:从源头规避性能问题
Redis 性能问题的根源,90% 都来自于不合理的缓存策略设计。优秀的缓存策略能从源头规避绝大多数线上问题,而不是事后通过参数调优来弥补。
项目核心依赖配置
本文所有代码基于SpringBoot 3.2.4 实现,核心maven依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam</groupId>
<artifactId>redis-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-demo</name>
<description>Redis调优实战demo</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<redisson.version>3.27.0</redisson.version>
<fastjson2.version>2.0.52</fastjson2.version>
<lombok.version>1.18.30</lombok.version>
<springdoc.version>2.5.0</springdoc.version>
<caffeine.version>3.1.8</caffeine.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-transaction</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.1 缓存更新策略的选型与落地
缓存与数据库的一致性是缓存设计的核心,不同的更新策略直接决定了数据一致性、读写性能与实现复杂度,主流的更新策略分为以下4种:
1.1.1 Cache Aside 模式
Cache Aside 是业务开发中最常用的缓存模式,核心逻辑是业务代码直接管理缓存与数据库,缓存只作为查询的加速层。
核心逻辑:
- 读操作:先查缓存,未命中则查数据库,查询成功后写入缓存再返回
- 写操作:先更新数据库,更新成功后删除缓存(而非更新缓存)
选择删除缓存而非更新缓存,核心是避免并发场景下的脏数据问题:若采用更新缓存,线程A更新数据库后,线程B同时更新数据库并更新缓存,线程A再更新缓存,会导致缓存中存储的是线程A的旧数据,出现数据不一致。而删除缓存可以让下一次读请求主动拉取最新的数据库数据,最大程度降低并发冲突的概率。
以下是代码实现:
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 用户实体类
* @author ken
*/
@Data
@TableName("t_user")
@Schema(description = "用户实体")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
@Schema(description = "用户ID", example = "1")
private Long id;
@Schema(description = "用户名", example = "jam")
private String username;
@Schema(description = "年龄", example = "25")
private Integer age;
@Schema(description = "邮箱", example = "jam@demo.com")
private String email;
}
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 用户Mapper接口
* @author ken
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 查询所有用户ID
* @return 用户ID列表
*/
@Select("select id from t_user")
List<Long> selectAllUserId();
}
对应的MySQL 8.0 表结构SQL:
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(64) NOT NULL COMMENT '用户名',
`age` int DEFAULT NULL COMMENT '年龄',
`email` varchar(128) DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
package com.jam.demo.service;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
/**
* 用户服务实现类
* @author ken
*/
@Slf4j
@Service
public class UserService {
private static final String USER_CACHE_PREFIX = "user:info:";
private static final long CACHE_EXPIRE_TIME = 3600L;
private final UserMapper userMapper;
private final RedissonClient redissonClient;
private final PlatformTransactionManager transactionManager;
private final BloomFilterService bloomFilterService;
public UserService(UserMapper userMapper, RedissonClient redissonClient, PlatformTransactionManager transactionManager, BloomFilterService bloomFilterService) {
this.userMapper = userMapper;
this.redissonClient = redissonClient;
this.transactionManager = transactionManager;
this.bloomFilterService = bloomFilterService;
}
/**
* 根据用户ID查询用户信息(Cache Aside读模式)
* @param userId 用户ID
* @return 用户实体
*/
public User getUserById(Long userId) {
if (!bloomFilterService.userIdExists(userId)) {
return null;
}
String cacheKey = USER_CACHE_PREFIX + userId;
RBucket<String> bucket = redissonClient.getBucket(cacheKey);
String cacheValue = bucket.get();
if (StringUtils.hasText(cacheValue)) {
return "NULL".equals(cacheValue) ? null : JSON.parseObject(cacheValue, User.class);
}
User user = userMapper.selectById(userId);
if (!ObjectUtils.isEmpty(user)) {
bucket.set(JSON.toJSONString(user), CACHE_EXPIRE_TIME, TimeUnit.SECONDS);
} else {
bucket.set("NULL", 60L, TimeUnit.SECONDS);
}
return user;
}
/**
* 更新用户信息(Cache Aside写模式,编程式事务)
* @param user 用户实体
* @return 更新结果
*/
public boolean updateUser(User user) {
if (ObjectUtils.isEmpty(user) || ObjectUtils.isEmpty(user.getId())) {
return false;
}
TransactionDefinition definition = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(definition);
try {
int updateCount = userMapper.updateById(user);
if (updateCount > 0) {
transactionManager.commit(status);
String cacheKey = USER_CACHE_PREFIX + user.getId();
redissonClient.getBucket(cacheKey).delete();
return true;
}
transactionManager.rollback(status);
return false;
} catch (Exception e) {
transactionManager.rollback(status);
log.error("更新用户信息异常,userId:{}", user.getId(), e);
return false;
}
}
}
1.1.2 其他更新策略选型对比
| 策略 | 核心逻辑 | 一致性 | 性能 | 适用场景 |
| Read Through | 读操作由缓存层封装,未命中时缓存层自动加载数据库数据 | 中 | 中 | 读多写少,代码层不想管理缓存逻辑 |
| Write Through | 写操作由缓存层封装,同时更新缓存与数据库,双写成功才返回 | 高 | 低 | 写操作频繁,一致性要求极高 |
| Write Back | 写操作只更新缓存,异步批量刷入数据库 | 低 | 极高 | 计数、非核心数据,可容忍一定数据丢失 |
1.2 缓存粒度控制:平衡内存占用与读写性能
缓存粒度指的是缓存数据的范围,不合理的粒度设计会导致内存浪费、读写性能下降、并发冲突加剧。
1.2.1 粒度设计的核心原则
- 最小可用原则:只缓存业务需要的字段,而非整个对象的全量字段
- 读写匹配原则:缓存的粒度要与业务的读写频率匹配,避免频繁更新整个大对象
1.2.2 不同粒度的实现与对比
以用户信息为例,对比两种主流的存储方式:
- String 全量序列化存储将整个用户对象序列化为 JSON 字符串,用 String 类型存储,适合读多写少、几乎不会更新单个字段的场景。 优点:读写简单,一次操作即可完成 缺点:更新单个字段需要重写整个对象,浪费带宽与CPU,内存占用较高
- Hash 分字段存储将用户对象的每个字段作为 Hash 的 field 存储,适合写多、频繁更新单个字段的场景。 优点:更新单个字段无需操作整个对象,节省带宽与CPU,内存占用更低 缺点:读取多个字段需要多次操作或 HMGET,代码复杂度略高
内存占用实测(100万条相同的用户数据):
| 存储方式 | 内存占用 | 单字段更新耗时 | 全量读取耗时 |
| String JSON | 218MB | 1.2ms | 0.3ms |
| Hash 分字段 | 146MB | 0.4ms | 0.5ms |
1.3 热点Key优化方案
热点Key指的是短时间内被大量请求访问的Key,比如秒杀活动的商品库存、热门榜单数据。热点Key会导致Redis单节点CPU负载飙升,甚至触发缓存击穿,严重影响服务稳定性。
1.3.1 热点Key的识别
- Redis 7.0+ 内置
hotkeys命令,可直接扫描出当前实例的热点Key - 业务层埋点统计,记录每个Key的访问频率
- 基于Redis的
monitor命令临时采样分析(仅适用于测试环境,线上禁用)
1.3.2 热点Key的优化方案
核心思路是分散压力+多级缓存,将热点Key的访问压力从Redis单节点分散到应用本地,避免Redis被打满。
以下是实现代码:
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 商品实体类
* @author ken
*/
@Data
@TableName("t_product")
@Schema(description = "商品实体")
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
@Schema(description = "商品ID", example = "1")
private Long id;
@Schema(description = "商品名称", example = "智能手机")
private String productName;
@Schema(description = "商品价格", example = "2999.00")
private BigDecimal price;
@Schema(description = "库存数量", example = "10000")
private Integer stock;
}
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.Product;
import org.apache.ibatis.annotations.Mapper;
/**
* 商品Mapper接口
* @author ken
*/
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
}
package com.jam.demo.dto;
import com.alibaba.fastjson2.annotation.JSONField;
import com.jam.demo.entity.Product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 商品缓存DTO
* @author ken
*/
@Data
@Schema(description = "商品缓存DTO")
public class ProductCacheDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "商品数据")
private Product product;
@Schema(description = "逻辑过期时间戳")
@JSONField(name = "expire_time")
private Long expireTime;
}
package com.jam.demo.service;
import com.alibaba.fastjson2.JSON;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.jam.demo.dto.ProductCacheDTO;
import com.jam.demo.entity.Product;
import com.jam.demo.mapper.ProductMapper;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 商品服务实现类
* @author ken
*/
@Slf4j
@Service
public class ProductService {
private static final String PRODUCT_CACHE_PREFIX = "product:info:";
private static final long LOCAL_CACHE_EXPIRE_SECONDS = 5L;
private static final long REDIS_CACHE_EXPIRE_SECONDS = 60L;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = new ThreadPoolExecutor(
5,
10,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "cache-rebuild-thread-" + count.getAndIncrement());
thread.setDaemon(true);
return thread;
}
}
);
private final ProductMapper productMapper;
private final RedissonClient redissonClient;
private final Cache<Long, Product> localCache;
public ProductService(ProductMapper productMapper, RedissonClient redissonClient) {
this.productMapper = productMapper;
this.redissonClient = redissonClient;
this.localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(LOCAL_CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS)
.build();
}
/**
* 根据商品ID查询商品信息(多级缓存优化热点Key)
* @param productId 商品ID
* @return 商品实体
*/
public Product getProductById(Long productId) {
if (ObjectUtils.isEmpty(productId)) {
return null;
}
Product product = localCache.getIfPresent(productId);
if (!ObjectUtils.isEmpty(product)) {
return product;
}
String cacheKey = PRODUCT_CACHE_PREFIX + productId;
RBucket<String> bucket = redissonClient.getBucket(cacheKey);
String cacheValue = bucket.get();
if (ObjectUtils.isEmpty(cacheValue)) {
String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS);
if (!locked) {
TimeUnit.MILLISECONDS.sleep(100);
return getProductById(productId);
}
cacheValue = bucket.get();
if (StringUtils.hasText(cacheValue)) {
product = JSON.parseObject(cacheValue, Product.class);
localCache.put(productId, product);
return product;
}
product = productMapper.selectById(productId);
if (!ObjectUtils.isEmpty(product)) {
bucket.set(JSON.toJSONString(product), REDIS_CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
localCache.put(productId, product);
}
return product;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取分布式锁异常,productId:{}", productId, e);
return null;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
product = JSON.parseObject(cacheValue, Product.class);
localCache.put(productId, product);
return product;
}
/**
* 根据商品ID查询商品信息(逻辑过期方案解决缓存击穿)
* @param productId 商品ID
* @return 商品实体
*/
public Product getProductByIdWithLogicExpire(Long productId) {
if (ObjectUtils.isEmpty(productId)) {
return null;
}
String cacheKey = PRODUCT_CACHE_PREFIX + productId;
RBucket<String> bucket = redissonClient.getBucket(cacheKey);
String cacheValue = bucket.get();
if (!StringUtils.hasText(cacheValue)) {
return null;
}
ProductCacheDTO cacheDTO = JSON.parseObject(cacheValue, ProductCacheDTO.class);
Product product = cacheDTO.getProduct();
Long expireTime = cacheDTO.getExpireTime();
if (System.currentTimeMillis() < expireTime) {
return product;
}
String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
boolean locked = lock.tryLock();
if (locked) {
try {
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
Product newProduct = productMapper.selectById(productId);
if (!ObjectUtils.isEmpty(newProduct)) {
ProductCacheDTO newCacheDTO = new ProductCacheDTO();
newCacheDTO.setProduct(newProduct);
newCacheDTO.setExpireTime(System.currentTimeMillis() + REDIS_CACHE_EXPIRE_SECONDS * 1000);
bucket.set(JSON.toJSONString(newCacheDTO));
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
});
} catch (Exception e) {
log.error("缓存重建任务提交异常,productId:{}", productId, e);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
return product;
}
}
核心优化点:
- 本地缓存使用Caffeine,基于LRU-WFM淘汰算法,性能远超传统本地缓存方案
- 本地缓存设置较短的过期时间,平衡数据一致性与性能
- 热点请求优先命中本地缓存,无需访问Redis,极大降低Redis的压力
- 本地缓存设置最大容量,避免内存溢出
二、内存淘汰机制深度调优:守住Redis的生命线
Redis是纯内存数据库,内存是其性能的核心载体。不合理的内存管理会导致OOM、内存碎片、频繁淘汰触发主线程阻塞,严重影响服务稳定性。
2.1 Redis内存模型与核心指标解读
Redis的内存占用主要分为4个部分:
- 数据内存:存储业务数据的内存,占比最高,是调优的核心
- 缓冲内存:包括客户端缓冲区、复制积压缓冲区、AOF缓冲区
- 进程内存:Redis进程本身运行占用的内存,占比极低
- 内存碎片:已释放但无法被重新利用的内存空间,由频繁的更新删除操作导致
通过 info memory 命令可查看核心内存指标,关键指标解读:
| 指标 | 含义 | 健康阈值 |
| used_memory | Redis分配器分配的总内存(含数据、缓冲等) | 不超过maxmemory的90% |
| used_memory_rss | 操作系统视角看到的Redis进程占用的物理内存 | 与used_memory差值越小越好 |
| mem_fragmentation_ratio | 内存碎片率(used_memory_rss / used_memory) | 1.0~1.2为健康,超过1.5说明碎片严重 |
| used_memory_peak | 内存使用峰值 | 用于评估最大内存需求 |
2.2 内存淘汰策略的底层逻辑与选型
当Redis的used_memory达到maxmemory阈值时,会触发内存淘汰机制,根据配置的淘汰策略删除符合条件的Key,释放内存空间。
Redis 7.0 提供8种淘汰策略,分为4大类:
2.2.1 LRU系列策略(基于最近使用时间)
- volatile-lru:从设置了过期时间的Key中,淘汰最近最少使用的Key
- allkeys-lru:从所有Key中,淘汰最近最少使用的Key
底层实现:Redis采用近似LRU算法,而非严格的LRU双向链表。严格LRU需要为每个Key维护双向链表,内存开销大、性能损耗高。近似LRU通过随机采样maxmemory-samples个Key,淘汰其中最久未使用的Key,Redis 7.0 引入了LRU池化优化,采样结果存入池化结构,淘汰池化结构中最久未使用的Key,效果无限接近严格LRU。
2.2.2 LFU系列策略(基于访问频率)
- volatile-lfu:从设置了过期时间的Key中,淘汰访问频率最低的Key
- allkeys-lfu:从所有Key中,淘汰访问频率最低的Key
底层实现:LFU通过计数器记录每个Key的访问频率,计数器采用对数增长模式,访问频率越高,计数器增长越慢;同时设置衰减周期,每隔一段时间计数器衰减,避免历史热点Key长期占用内存。核心参数:
lfu-log-factor:计数器增长因子,值越小,计数器增长越快lfu-decay-time:计数器衰减周期,单位为分钟,默认1分钟
2.2.3 其他策略
- volatile-random:从设置了过期时间的Key中随机淘汰
- allkeys-random:从所有Key中随机淘汰
- volatile-ttl:从设置了过期时间的Key中,淘汰即将过期的Key
- noeviction:不淘汰任何Key,内存满时拒绝所有写操作,返回OOM错误(默认策略)
2.2.4 淘汰策略选型指南
| 业务场景 | 推荐策略 | 核心理由 |
| 读多写少,热点数据稳定 | allkeys-lru | 保证热点数据常驻内存,提升缓存命中率 |
| 热点数据波动大,有明显的冷热区分 | allkeys-lfu | 优先保留高频访问的Key,淘汰低频访问的Key |
| 有明确的过期时间,部分数据需要永久存储 | volatile-lru/volatile-lfu | 只淘汰有过期时间的Key,避免永久存储的核心数据被误删 |
| 数据权重一致,无明显冷热区分 | allkeys-random | 实现简单,性能损耗最低 |
| 不允许数据丢失,核心数据存储 | noeviction | 避免核心数据被淘汰,通过监控提前扩容 |
2.3 内存调优核心参数配置
以下是Redis 7.0 生产环境核心内存参数的最佳实践配置:
# 最大内存限制,根据服务器内存配置,建议不超过物理内存的50%
maxmemory 10G
# 淘汰策略,根据业务场景选择
maxmemory-policy allkeys-lru
# 采样数量,默认5,设置为10时效果接近严格LRU,性能损耗可忽略
maxmemory-samples 10
# 主动碎片整理开关,Redis 4.0+支持,开启后自动整理内存碎片
activedefrag yes
# 触发碎片整理的最小碎片内存
active-defrag-ignore-bytes 100mb
# 触发碎片整理的碎片率阈值
active-defrag-threshold-lower 10
# 碎片整理最大占用CPU比例,避免影响主线程
active-defrag-cycle-max 25
# LFU参数配置
lfu-log-factor 10
lfu-decay-time 1
2.4 数据结构的内存优化实践
不同的数据结构有不同的内存编码方式,选择合适的编码方式可以极大降低内存占用,提升性能。
2.4.1 String类型优化
String是Redis最常用的数据类型,有3种编码方式:
- int编码:存储8字节以内的长整型数字,占用8字节内存
- embstr编码:存储小于等于44字节的字符串,采用连续内存分配,内存开销小,缓存友好
- raw编码:存储大于44字节的字符串,采用分离的内存分配,内存开销大
优化建议:
- 数字类型优先用整数存储,而非字符串
- 短字符串优先控制在44字节以内,使用embstr编码
- 长字符串优先压缩后存储,比如用Snappy、LZ4压缩算法
2.4.2 复合类型优化
Hash、List、Set、Sorted Set等复合类型,在元素数量少、元素值小的时候,会采用紧凑的listpack编码(Redis 7.0 替代了旧的ziplist),内存占用极低;当元素数量或大小超过阈值时,会转换为传统的结构编码,内存占用大幅上升。
核心阈值配置:
# Hash类型listpack编码阈值
hash-max-listpack-entries 512
hash-max-listpack-value 64
# List类型listpack编码阈值
list-max-listpack-size -2
# Set类型intset编码阈值
set-max-intset-entries 512
# Sorted Set类型listpack编码阈值
zset-max-listpack-entries 128
zset-max-listpack-value 64
优化建议:
- 小对象优先用Hash存储,而非String序列化,内存占用可降低30%以上
- List类型优先用Redis 5.0+的quicklist结构,平衡内存占用与读写性能
- 整数集合优先用Set的intset编码,内存占用远低于普通hashtable编码
三、Redis三大经典问题根治方案
缓存穿透、缓存击穿、缓存雪崩是Redis缓存场景下最高发的三大问题,轻则导致接口超时,重则导致数据库被打挂,服务全线崩溃。本文从底层原理出发,提供全场景的根治方案。
3.1 缓存穿透:彻底解决无效请求打穿数据库
3.1.1 底层原理与根因
缓存穿透指的是请求的数据在缓存和数据库中都不存在,导致每次请求都必须穿透缓存层,直接访问数据库。当短时间内出现大量此类请求时,会给数据库带来巨大压力,甚至被打挂。
核心根因:
- 业务逻辑漏洞,传入非法的参数
- 恶意攻击,用不存在的Key发起大量请求
- 数据清理后,缓存与数据库中都不存在对应的数据
3.1.2 根治方案
方案1:空值缓存
对于数据库中不存在的数据,在缓存中写入一个空值,并设置较短的过期时间,避免后续相同的请求再次访问数据库。核心实现已集成在1.1.1的UserService.getUserById方法中,通过特殊标记"NULL"标识空值,设置60秒过期时间,平衡内存占用与防护效果。
适用场景:非法请求参数固定、重复请求概率高的场景优点:实现简单,成本极低缺点:会占用一定的缓存空间,存在短时间的数据不一致
方案2:布隆过滤器
布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否存在于集合中。它的核心特性是:如果布隆过滤器判断元素不存在,那么元素一定不存在;如果判断元素存在,有极小的概率误判。
基于这个特性,我们可以将数据库中所有合法的Key预加载到布隆过滤器中,请求到来时先查询布隆过滤器,若判断不存在,直接返回,无需访问缓存和数据库,从根源上解决缓存穿透。
以下是实现代码:
package com.jam.demo.service;
import com.jam.demo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.List;
/**
* 布隆过滤器服务
* @author ken
*/
@Slf4j
@Service
public class BloomFilterService implements CommandLineRunner {
private static final String USER_BLOOM_FILTER_NAME = "user:id:bloom";
private static final long EXPECTED_INSERTIONS = 1000000L;
private static final double FPP = 0.001;
private final RedissonClient redissonClient;
private final UserMapper userMapper;
private RBloomFilter<Long> userBloomFilter;
public BloomFilterService(RedissonClient redissonClient, UserMapper userMapper) {
this.redissonClient = redissonClient;
this.userMapper = userMapper;
}
@Override
public void run(String... args) {
userBloomFilter = redissonClient.getBloomFilter(USER_BLOOM_FILTER_NAME);
if (!userBloomFilter.isExists()) {
userBloomFilter.tryInit(EXPECTED_INSERTIONS, FPP);
List<Long> userIdList = userMapper.selectAllUserId();
if (!ObjectUtils.isEmpty(userIdList)) {
userIdList.forEach(userBloomFilter::add);
}
log.info("用户ID布隆过滤器初始化完成,共加载{}个用户ID", userIdList.size());
}
}
/**
* 判断用户ID是否存在
* @param userId 用户ID
* @return 是否存在
*/
public boolean userIdExists(Long userId) {
if (ObjectUtils.isEmpty(userId)) {
return false;
}
return userBloomFilter.contains(userId);
}
/**
* 新增用户ID到布隆过滤器
* @param userId 用户ID
*/
public void addUserId(Long userId) {
if (!ObjectUtils.isEmpty(userId)) {
userBloomFilter.add(userId);
}
}
}
核心参数说明:
expectedInsertions:预期插入的元素数量fpp:误判率,值越小,布隆过滤器占用的内存越大,误判率越低
适用场景:数据量稳定、新增频率低、恶意攻击风险高的场景优点:内存占用极低,过滤效果好,从根源上拦截无效请求缺点:不支持删除元素,数据新增时需要同步更新布隆过滤器
3.2 缓存击穿:解决热点Key过期的并发冲击
3.2.1 底层原理与根因
缓存击穿指的是某个热点Key在过期的瞬间,出现大量并发请求,这些请求无法命中缓存,全部直接访问数据库,导致数据库压力瞬间飙升,甚至被打挂。
核心根因:
- 热点Key设置了过期时间,过期瞬间并发请求量极大
- 缓存重建耗时较长,并发场景下多个线程同时重建缓存,重复访问数据库
3.2.2 根治方案
方案1:分布式互斥锁
核心逻辑是:当缓存未命中时,只有一个线程能获取分布式锁,去数据库加载数据并重建缓存,其他线程等待锁释放后,重新查询缓存,避免多个线程同时访问数据库。完整实现已集成在1.3.2的ProductService.getProductById方法中。
核心优化点:
- 采用双重检查锁(DCL),获取锁后再次查询缓存,避免锁等待期间缓存已经被重建
- 锁设置了等待时间和超时时间,避免死锁
- 未获取到锁的线程休眠后重试,避免CPU空轮询
适用场景:数据一致性要求高、并发量不是极致高的场景优点:实现简单,数据一致性强,无脏数据缺点:线程需要等待锁,性能有一定损耗,高并发场景下可能出现线程阻塞
方案2:逻辑过期
核心逻辑是:热点Key不设置物理过期时间,在Value中存储逻辑过期时间;查询时如果逻辑时间未过期,直接返回数据;如果逻辑时间已过期,立刻返回旧数据,同时开启异步线程去重建缓存,不会有任何线程等待,极致的性能。完整实现已集成在1.3.2的ProductService.getProductByIdWithLogicExpire方法中。
核心优化点:
- 异步线程池重建缓存,主线程无需等待,直接返回旧数据,无性能损耗
- 分布式锁控制只有一个线程重建缓存,避免重复访问数据库
- 线程池设置核心参数,避免无限制创建线程
- 即使缓存重建失败,也能返回旧数据,不影响用户体验
适用场景:高并发秒杀、热门榜单等极致性能要求、可容忍短时间数据不一致的场景优点:无线程等待,性能极致,不会出现数据库压力飙升缺点:实现复杂度高,存在短时间的数据不一致,内存占用略高
3.3 缓存雪崩:避免全量请求冲击数据库
3.3.1 底层原理与根因
缓存雪崩指的是短时间内大量缓存Key同时过期,或者Redis集群出现大面积宕机,导致所有请求都直接访问数据库,数据库压力瞬间拉满,最终宕机,引发服务全线崩溃。
核心根因:
- 大量Key设置了相同的过期时间,到期同时失效
- Redis集群单点故障,无高可用架构,导致整个缓存层不可用
- 缓存降级、熔断机制缺失,数据库无法承受突发的流量冲击
3.3.2 全链路根治方案
方案1:过期时间打散
核心逻辑是:给每个Key的过期时间增加一个随机值,避免大量Key同时过期,将过期时间均匀分散在不同的时间点,降低数据库的压力。
实现示例:
private static final long BASE_EXPIRE_TIME = 3600L;
private static final long RANDOM_EXPIRE_RANGE = 600L;
public void setCache(String key, Object value) {
long randomTime = ThreadLocalRandom.current().nextLong(RANDOM_EXPIRE_RANGE);
long expireTime = BASE_EXPIRE_TIME + randomTime;
redissonClient.getBucket(key).set(JSON.toJSONString(value), expireTime, TimeUnit.SECONDS);
}
核心优化点:
- 基础过期时间+随机偏移量,将过期时间分散在1小时到1小时10分钟之间
- 采用ThreadLocalRandom生成随机数,性能远超Random
- 随机范围根据业务场景调整,一般为基础过期时间的10%~30%
方案2:Redis集群高可用架构
核心逻辑是:构建主从+哨兵+Redis Cluster集群架构,避免单点故障,实现故障自动转移,保证缓存层的高可用性。
核心架构说明:
- Redis Cluster采用3主3从架构,每个主节点负责一部分槽位,数据分片存储
- 哨兵集群监控所有主从节点,主节点故障时自动将从节点升级为主节点,实现故障自动转移
- 客户端采用集群模式接入,自动感知节点变化,请求路由到正确的节点
方案3:服务熔断与降级
核心逻辑是:当数据库的请求量、错误率达到阈值时,触发熔断,直接返回降级数据,保护数据库不被打挂,待服务恢复后自动关闭熔断。
方案4:多级缓存架构
构建「本地缓存 -> Redis集群 -> 数据库」的多级缓存架构,即使Redis集群出现故障,本地缓存也能承接大部分热点请求,避免所有请求直接打到数据库。
四、Redis高性能调优的其他核心维度
4.1 网络IO模型调优
Redis的性能瓶颈很多时候来自于网络IO,合理的网络参数配置可以极大提升高并发场景下的性能。
# TCP backlog参数,控制TCP连接的等待队列长度,高并发场景下调大
tcp-backlog 1024
# 客户端空闲超时时间,0表示不超时,避免频繁的连接创建销毁
timeout 0
# TCP keepalive参数,检测死连接,间隔300秒
tcp-keepalive 300
# 禁用TCP延迟发送,减少网络延迟
tcp-nodelay yes
4.2 持久化机制调优
Redis的持久化机制(RDB/AOF)是影响性能的核心因素,不合理的持久化配置会导致主线程频繁阻塞,甚至服务不可用。
4.2.1 混合持久化配置(推荐)
Redis 4.0+ 支持混合持久化,结合了RDB和AOF的优点,是生产环境的首选方案。
# 开启AOF持久化
appendonly yes
# AOF刷盘策略,everysec平衡性能与安全性,每秒刷盘一次
appendfsync everysec
# 开启混合持久化
aof-use-rdb-preamble yes
# AOF重写触发阈值,文件大小增长100%时触发重写
auto-aof-rewrite-percentage 100
# 触发AOF重写的最小文件大小
auto-aof-rewrite-min-size 512mb
# RDB持久化策略,根据业务场景调整,避免频繁fork
save 3600 1
save 300 100
save 60 10000
# 关闭RDB持久化时的文件校验,提升fork速度
rdbchecksum no
4.2.2 持久化调优核心原则
- 禁止在主节点开启持久化,持久化操作放在从节点执行,避免主节点主线程阻塞
- 控制Redis实例的内存大小,建议不超过20G,避免fork子进程耗时过长阻塞主线程
- 关闭Linux的透明大页(THP),THP会导致fork子进程时内存页复制耗时大幅增加,阻塞主线程
- 避免在业务高峰期执行AOF重写和RDB持久化操作
4.3 命令使用规范与性能坑
- 禁止线上使用
KEYS *、FLUSHDB、FLUSHALL命令,KEYS *会遍历所有Key,导致主线程长时间阻塞,线上用SCAN命令替代 - 避免使用
HGETALL、SMEMBERS、ZRANGE等操作全量元素的命令,元素数量过多时会导致主线程阻塞,用HSCAN、SSCAN、ZSCAN分批遍历替代 - 批量操作优先使用
PIPELINE、MSET、MGET命令,减少网络IO次数,提升性能 - 避免在Redis中存储大Key,单个Key的value大小建议不超过10KB,大Key会导致网络传输耗时增加、内存碎片加剧、持久化阻塞
- 避免频繁的小更新操作,优先用批量操作,减少主线程的压力
五、Redis调优核心原则
Redis高性能调优从来不是盲目堆砌参数,而是遵循「业务设计优先,底层逻辑为本」的核心原则:
- 源头优先原则:90%的性能问题都来自于不合理的业务设计,优先优化缓存策略、Key设计、数据结构,而非参数调优
- 内存为本原则:Redis是内存数据库,内存是其生命线,所有调优都要围绕内存优化展开,降低内存占用,减少内存碎片
- 主线程保护原则:Redis的命令执行是单线程的,所有可能阻塞主线程的操作都要严格规避,比如大Key操作、频繁fork、慢查询
- 高可用兜底原则:任何调优都不能忽略高可用设计,构建多级缓存、集群高可用、熔断降级机制,避免单点故障引发全线崩溃
只有从底层理解Redis的运行逻辑,结合业务场景设计合理的缓存策略,才能真正发挥Redis的极致性能,规避线上高频出现的各类问题。