别再啃理论了!30分钟带你写出能上线的 Redis 代码

装饰图


专栏导读:Redis 从入门到生产避坑:缓存架构实战指南,共7天带你从入门到精通。已发布0篇。


天数文章标题状态
第1天待发布当前文章
第2天待发布敬请期待
第3天待发布敬请期待
第4天待发布敬请期待
第5天待发布敬请期待
第6天待发布敬请期待
第7天待发布敬请期待

装饰图

Redis封面


别再啃理论了!30分钟带你写出能上线的 Redis 代码


凌晨三点,我接到了运维的电话

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:缓存与数据库双写不一致

更新数据时,先删缓存还是先更新数据库?两种顺序都有极端情况导致脏数据。最稳妥的方式是用延迟双删策略,或者听专栏后续的分布式缓存一致性方案。


高级进阶:代码写对了只是开始

今天咱们搞定了环境、基本读写、集群和监控,但其实还差两个重要的能力没有深挖:

  1. 缓存穿透/击穿/雪崩:你目前的代码只是对穿透做了空值处理,但真正的高手会用布隆过滤器,会用分布式锁防击穿,会用多级缓存和预热防雪崩。
  2. 分布式锁:你不是要在同一个Key上做互斥吗?Redisson开箱即用,但背后RedLock算法的坑呢?锁续期问题呢?我曾在生产上因为锁没释放导致整个业务阻塞。

这些会在专栏后续的文章中逐个拆解,配合大量压测数据和线上案例,让你看完就能落地。


这篇只是冰山一角

老板后来问我:“这次事故能避免吗?” 我说能,如果早按今天这套方案来,绝对不会挂得这么惨。

今天讲的单机、集群、监控和运维三板斧,够你快速搭建一个能上线的Redis服务。但离资深架构师还差得远,30天专栏我会带你从基础一直打到高级:分布式锁源码剖析、秒杀场景缓存优化、100万QPS下的Redis架构设计、热Key自动探测系统……这些不仅让你面试能聊,更能在实际项目中扛住并发压力。

觉得有用就点个赞,想系统性拿下Redis的,关注专栏更新,下一篇我们直接干缓存穿透和击穿的实战方案,用布隆过滤器把DB请求量砍掉99%。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值