
专栏导读:Redis 从入门到生产避坑:缓存架构实战指南,共7天带你从入门到精通。已发布0篇。
| 天数 | 文章标题 | 状态 |
|---|---|---|
| 第1天 | 待发布 | 当前文章 |
| 第2天 | 待发布 | 敬请期待 |
| 第3天 | 待发布 | 敬请期待 |
| 第4天 | 待发布 | 敬请期待 |
| 第5天 | 待发布 | 敬请期待 |
| 第6天 | 待发布 | 敬请期待 |
| 第7天 | 待发布 | 敬请期待 |

文章目录

别再啃理论了!30分钟带你写出能上线的 Redis 代码
凌晨三点,我接到了运维的电话

上周三凌晨两点多,睡得正香,手机突然狂震,迷迷糊糊接起来,运维兄弟声音都是抖的:"哥,订单接口超时了,数据库CPU飙到 100%,Redis服务好像死掉了,你快看看!"
我一个激灵爬起来,连上VPN一看,好家伙,Redis进程确实没了,所有请求直接砸到MySQL上,瞬间把连接池打满,整个下单链路全部瘫痪。老板在群里@我:"什么情况?!!" 那分钟血压绝对180。
折腾了半小时排查出来原因,简直想扇自己两耳光——线上Redis是默认配置跑的,没设密码,没开持久化,监控也没有,机器重启后数据全丢,缓存雪崩把数据库打穿。这次事故直接导致公司损失了十几万订单。
你是不是也有过这种经历?面试的时候背了一堆Redis八股文,什么跳表、SDS、集群原理,可是一上手搭环境、写代码就各种掉坑,要么序列化报错,要么连接池耗尽,要么内存爆了不知道怎么查。
讲真,Redis这东西不难,难的是从来没系统跑通过一套生产级的代码。 今天我就用踩过的坑,带你30分钟写出一份能直接部署到线上的Spring Boot + Redis代码,全程复制粘贴就能跑,从单机到集群,从健康检查到运维命令,一条龙给你安排明白。
先从最基础的来:装个Redis不过5分钟
别用Windows装Redis了,直接上Docker,一行命令的事儿。我平时开发就用docker-compose,方便管理。
version: '3.8'
services:
redis:
image: redis:7-alpine
container_name: my-redis
ports:
- "6379:6379"
command: redis-server --requirepass myStrongPass123
volumes:
- ./data:/data
restart: always
🔴 血的教训:千万别用空密码,也别把密码写成
123456。上次我的测试环境被人扫到,成了挖矿肉鸡,CPU跑满100%,排查了半天才发现端口对外暴露了。
把上面内容保存成docker-compose.yml,在你项目根目录执行:
docker-compose up -d
就这么简单,一个带密码、数据持久化到宿主机、自启的Redis就起来了。用docker ps看一眼:
CONTAINER ID IMAGE STATUS PORTS NAMES
abc123def456 redis:7-alpine Up 10 seconds 0.0.0.0:6379->6379/tcp my-redis
后面所有代码就是以这个实例为基础的。
Spring Boot集成Redis:别再用RedisTemplate的坑了
新建一个Spring Boot 3.x项目,pom.xml加上依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Jackson序列化,生产必备 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
配置application.yml:
spring:
data:
redis:
host: localhost
port: 6379
password: myStrongPass123
timeout: 3000ms
lettuce:
pool:
max-active: 32
max-idle: 16
min-idle: 8
max-wait: 2000ms
🚫 别用Jedis了,现在Spring Boot默认Lettuce,底层Netty,连接复用和异步性能好太多。我前年把老项目的Jedis换成Lettuce后,缓存层QPS涨了40%。
接下来写个配置类,把坑填上——RedisTemplate默认用的JDK序列化,对象必须实现Serializable,存到Redis里是一堆二进制乱码,根本查不了。我们换成Jackson序列化,存的是JSON,人类可读,调试方便。
package com.demo.redis.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key用String序列化,可读性好
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value用Jackson JSON序列化,存对象不用实现Serializable
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
为什么这么做? 你存入一个User对象,在Redis里看到的是:
{"id":1,"name":"张三","age":25}
而不是一坨\xAC\xED\x00\x05t\x00...。出问题时直接redis-cli get key一看便知,线上排错能省80%时间。
写个能跑的缓存Service,直接复制就能用
下面是一个完整的、带缓存穿透保护的业务Service,存用户信息。
package com.demo.redis.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class UserCacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper;
private static final String USER_KEY_PREFIX = "user:info:";
private static final long CACHE_TTL_SECONDS = 3600; // 1小时
private static final String NULL_VALUE = "NULL"; // 防穿透的空值
/**
* 从缓存获取用户,没命中则查数据库
*/
public User getUser(Long userId) {
String cacheKey = USER_KEY_PREFIX + userId;
try {
Object value = redisTemplate.opsForValue().get(cacheKey);
if (value == null) {
return loadFromDbAndCache(userId, cacheKey);
}
// 防止缓存穿透的空值判断
if (NULL_VALUE.equals(value.toString())) {
return null;
}
// 将LinkedHashMap转换为User对象
return objectMapper.convertValue(value, User.class);
} catch (Exception e) {
log.error("读取Redis失败, key={}", cacheKey, e);
// 降级:直接查数据库
return loadFromDb(userId);
}
}
private User loadFromDbAndCache(Long userId, String cacheKey) {
User user = loadFromDb(userId);
if (user == null) {
// 缓存空对象防止穿透,过期时间要短
redisTemplate.opsForValue().set(cacheKey, NULL_VALUE,
60, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().set(cacheKey, user,
CACHE_TTL_SECONDS, TimeUnit.SECONDS);
}
return user;
}
private User loadFromDb(Long userId) {
// 模拟数据库查询,实际换成你的mapper调用
log.debug("查询数据库: userId={}", userId);
if (userId == 1) {
return new User(1L, "张三", 25);
}
return null;
}
public void updateUser(User user) {
// 先更新数据库,然后删除缓存,保证一致性
// updateDb(user);
String cacheKey = USER_KEY_PREFIX + user.getId();
redisTemplate.delete(cacheKey);
log.info("删除缓存: {}", cacheKey);
}
}
对应实体类:
package com.demo.redis.service;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
}
在Controller里调一下试试:
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
private final UserCacheService userCacheService;
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userCacheService.getUser(id);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(user);
}
}
启动项目后,用curl localhost:8080/user/1请求两次,第一次打印“查询数据库”,第二次就不会查了。你可以进Redis查看:
docker exec -it my-redis redis-cli -a myStrongPass123
127.0.0.1:6379> keys *
1) "user:info:1"
127.0.0.1:6379> get user:info:1
"{\"id\":1,\"name\":\"张三\",\"age\":25}"
大功告成。是不是很简单?但别高兴太早,这只是单机玩玩的水平,离上线还差得远。
集群:别等扛不住再搭,现在就配
单机Redis扛不住高并发,哪怕你开了持久化,一旦挂掉就会丢数据。生产环境至少要用Redis Cluster或者Sentinel。我推荐用Cluster,横向扩展简单,运维成本低。
在我早期的一个项目中,秒杀活动量上来,单机Redis达到10万QPS后响应时间飙升到500ms,压测发现单节点网络IO已到极限。连夜切到3主3从Cluster,QPS直接突破30万,RT降到3ms。
我们用Docker快速搭建一个3主3从最小集群,用于本地开发。
新建docker-compose-cluster.yml:
version: '3.8'
services:
redis-node-1:
image: redis:7-alpine
container_name: redis-node-1
ports: ["7001:7001", "17001:17001"]
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./cluster/7001/redis.conf:/usr/local/etc/redis/redis.conf
- ./cluster/7001/data:/data
networks:
redis-cluster:
ipv4_address: 173.18.0.2
redis-node-2:
image: redis:7-alpine
container_name: redis-node-2
ports: ["7002:7002", "17002:17002"]
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./cluster/7002/redis.conf:/usr/local/etc/redis/redis.conf
- ./cluster/7002/data:/data
networks:
redis-cluster:
ipv4_address: 173.18.0.3
redis-node-3:
image: redis:7-alpine
container_name: redis-node-3
ports: ["7003:7003", "17003:17003"]
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./cluster/7003/redis.conf:/usr/local/etc/redis/redis.conf
- ./cluster/7003/data:/data
networks:
redis-cluster:
ipv4_address: 173.18.0.4
redis-node-4:
image: redis:7-alpine
container_name: redis-node-4
ports: ["7004:7004", "17004:17004"]
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./cluster/7004/redis.conf:/usr/local/etc/redis/redis.conf
- ./cluster/7004/data:/data
networks:
redis-cluster:
ipv4_address: 173.18.0.5
redis-node-5:
image: redis:7-alpine
container_name: redis-node-5
ports: ["7005:7005", "17005:17005"]
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./cluster/7005/redis.conf:/usr/local/etc/redis/redis.conf
- ./cluster/7005/data:/data
networks:
redis-cluster:
ipv4_address: 173.18.0.6
redis-node-6:
image: redis:7-alpine
container_name: redis-node-6
ports: ["7006:7006", "17006:17006"]
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./cluster/7006/redis.conf:/usr/local/etc/redis/redis.conf
- ./cluster/7006/data:/data
networks:
redis-cluster:
ipv4_address: 173.18.0.7
networks:
redis-cluster:
driver: bridge
ipam:
config:
- subnet: 173.18.0.0/16
每个节点的redis.conf内容(以node-1为例,其他节点修改端口和cluster-announce-ip):
port 7001
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 5000
appendonly yes
requirepass myStrongPass123
masterauth myStrongPass123
cluster-announce-ip 173.18.0.2
cluster-announce-port 7001
cluster-announce-bus-port 17001
搭建集群:
# 启动所有节点
docker-compose -f docker-compose-cluster.yml up -d
# 进入任一节点执行集群创建
docker exec -it redis-node-1 redis-cli -a myStrongPass123 --cluster create \
173.18.0.2:7001 173.18.0.3:7002 173.18.0.4:7003 \
173.18.0.5:7004 173.18.0.6:7005 173.18.0.7:7006 \
--cluster-replicas 1
Spring Boot连接集群只需改配置:
spring:
data:
redis:
cluster:
nodes:
- 127.0.0.1:7001
- 127.0.0.1:7002
- 127.0.0.1:7003
- 127.0.0.1:7004
- 127.0.0.1:7005
- 127.0.0.1:7006
password: myStrongPass123
timeout: 3000ms
lettuce:
pool:
max-active: 32
max-idle: 16
min-idle: 8
业务代码不用动,依然用刚才的RedisTemplate,因为Lettuce已经自动适配了集群路由。
🔴 踩坑记录:集群模式下,Lettuce默认不跨slot执行多key命令(比如
keys *),如果需要批量操作,要么用hash tag把key落到同一个slot,要么用RedisTemplate.executePipelined。这个问题当初我排查了一整天,一直报CROSSSLOT Keys in request don't hash to the same slot,差点怀疑人生。
监控:看不到指标等于盲人开车
没有监控的Redis,就像没有仪表盘的汽车。你永远不知道什么时候内存会爆,连接池会不会满。Spring Boot Actuator + Micrometer能让我们低成本接入监控。
pom.xml添加:
配置:
management:
endpoints:
web:
exposure:
include: health,info,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: true
启动后访问/actuator/prometheus,你会看到一大堆Redis相关指标,比如:
lettuce_command_latency_seconds_count{command="GET",local="127.0.0.1:7001",remote="173.18.0.2:7001"} 1287.0
lettuce_command_latency_seconds_sum{command="GET",local="127.0.0.1:7001",remote="173.18.0.2:7001"} 0.529
最实用的几个指标:
- command_latency_seconds:Redis命令延迟,如果你看到p99突然变高,说明慢查询了。
- pool_idle_connections / pool_active_connections:连接池使用情况,如果active一直等于max,说明该扩池子了。
- redis_up:Redis节点是否存活。
这套对接Prometheus+Grafana,5分钟就能搭出一个漂亮的看板,我们线上就是用这个快速定位过一次内存碎片问题。
运维三板斧:备份、持久化、日常检查
Redis运维看似复杂,其实老司机日常就做三件事:备份、持久化检查、看INFO。
1. 持久化配置
生产必须同时开启RDB和AOF,取长补短。在redis.conf里加:
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
RDB做冷备,AOF保证秒级数据不丢。appendfsync everysec是性能和安全的平衡,千万别设成no,除非你对数据丢失无所谓。
2. 备份脚本
写个crontab每周做一次RDB文件备份到OSS,防止整个机器故障。
#!/bin/bash
BACKUP_DIR=/data/backup/redis
DATE=$(date +%Y%m%d)
cp /data/dump.rdb $BACKUP_DIR/dump_$DATE.rdb
# 上传到OSS或异地服务器
3. 日常检查命令
每天上班第一件事,连上Redis执行几个命令:
INFO memory # 看 used_memory_rss_human,别超过物理内存70%
INFO stats # 看 instantaneous_ops_per_sec,了解当前QPS
INFO replication # 主从同步状态,lag是否为0
SLOWLOG GET 10 # 慢查询日志,找出拖后腿的命令
去年双十一大促前,我通过SLOWLOG检查发现有个KEYS *扫描操作,在集群里特别慢,后来优化成SCAN,接口耗时从300ms降到10ms,避免了一次潜在事故。
避坑指南:这些雷我全替你踩过了
⚠️ 坑1:大Key问题
你缓存了一个几百KB甚至几MB的对象,比如商品详情页的全量JSON。一旦并发高了,网络带宽打满,拖垮整个集群。
提前约定: 大对象拆分成多个小Key,或压缩后存储。用redis-cli --bigkeys扫描大Key。
⚠️ 坑2:热Key问题
秒杀商品的缓存key,一秒几十万请求全砸到一个节点,单节点CPU爆满。
解决思路: 本地缓存(Caffeine) + Redis二级缓存;或者客户端做Key拆分(分散到多个副本)。专栏后面我会专门写一篇热Key的降级方案。
⚠️ 坑3:连接池配置不当
有次压测,每个请求都新建立连接,QPS才800就开始报Could not get a resource from the pool。后来发现忘了配置连接池参数。
牢记: max-active根据业务并发量设置,一般不超过数据库连接池的一半;max-wait别设太大,否则会把Tomcat线程池拖死。必须加上连接验证:
lettuce:
pool:
test-on-borrow: true
⚠️ 坑4:缓存与数据库双写不一致
更新数据时,先删缓存还是先更新数据库?两种顺序都有极端情况导致脏数据。最稳妥的方式是用延迟双删策略,或者听专栏后续的分布式缓存一致性方案。
高级进阶:代码写对了只是开始
今天咱们搞定了环境、基本读写、集群和监控,但其实还差两个重要的能力没有深挖:
- 缓存穿透/击穿/雪崩:你目前的代码只是对穿透做了空值处理,但真正的高手会用布隆过滤器,会用分布式锁防击穿,会用多级缓存和预热防雪崩。
- 分布式锁:你不是要在同一个Key上做互斥吗?Redisson开箱即用,但背后RedLock算法的坑呢?锁续期问题呢?我曾在生产上因为锁没释放导致整个业务阻塞。
这些会在专栏后续的文章中逐个拆解,配合大量压测数据和线上案例,让你看完就能落地。
这篇只是冰山一角
老板后来问我:“这次事故能避免吗?” 我说能,如果早按今天这套方案来,绝对不会挂得这么惨。
今天讲的单机、集群、监控和运维三板斧,够你快速搭建一个能上线的Redis服务。但离资深架构师还差得远,30天专栏我会带你从基础一直打到高级:分布式锁源码剖析、秒杀场景缓存优化、100万QPS下的Redis架构设计、热Key自动探测系统……这些不仅让你面试能聊,更能在实际项目中扛住并发压力。
觉得有用就点个赞,想系统性拿下Redis的,关注专栏更新,下一篇我们直接干缓存穿透和击穿的实战方案,用布隆过滤器把DB请求量砍掉99%。
56

被折叠的 条评论
为什么被折叠?



