Spring Boot整合Redis:从0到1打造电商系统高性能缓存方案

一、开篇:一个真实的电商秒杀场景

还记得双11那天吗?你守在手机前,零点一到就疯狂点击"立即抢购",结果页面转了半天,最后告诉你"系统繁忙,请稍后再试"。

作为开发者的你,可能知道背后的原因:流量太大了,数据库扛不住了!

想象一下,一个热门商品有10万人在抢,每秒1万次请求直接打到MySQL数据库,即使你的SQL写得再好,数据库也会瞬间崩溃。

这时候,Redis 就像一位"超级救火队员",能够扛住99%的请求,只让1%的真实订单请求打到数据库。这就是今天要讲的——缓存技术


二、什么是Redis?用大白话讲给你听

2.1 Redis是什么?

Redis = Remote Dictionary Server(远程字典服务器)

别被这个英文名吓到,你可以把它理解成:

Redis就是一台超级快的内存数据库,就像你电脑的记事本,但是这个记事本读写速度极快,而且能存很多种类型的数据。

2.2 为什么要用Redis?

来,我们用一个生活中的类比:

| 场景 | MySQL | Redis | |------|-------|-------| | 类比 | 图书馆(找书要走很远) | 书包(伸手就能拿到) | | 存储位置 | 硬盘(慢) | 内存(快) | | 读取速度 | 毫秒级 | 微秒级(快1000倍) | | 容量 | 大(TB级) | 小(GB级) | | 成本 | 低 | 高 |

一句话总结:Redis就是把热点数据放在内存里,让查询速度提升1000倍!

2.3 Redis能存什么?

Redis支持多种数据类型,就像你的书包里有不同的隔层:

  • String(字符串):存商品名称、价格等简单信息
  • Hash(哈希):存商品详情(多个字段)
  • List(列表):存商品评论、排行榜
  • Set(集合):存商品标签、点赞用户
  • ZSet(有序集合):存商品销量排行

三、为什么电商系统必须用Redis?

3.1 传统方案的问题

// ❌ 传统方案:每次都查数据库
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable Long id) {
    // 每次请求都查数据库,高并发下数据库会崩溃
    return productMapper.selectById(id);
}

问题:

  • 10万次请求 = 10万次数据库查询 = 数据库崩溃
  • 每次查询都要走磁盘IO,速度慢
  • 数据库连接池资源有限

3.2 Redis缓存方案的优势

// ✅ 优化方案:先查缓存,缓存没有再查数据库
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable Long id) {
    // 1. 先查Redis缓存(内存,速度快)
    Product product = redisTemplate.opsForValue().get("product:" + id);
    
    if (product != null) {
        return product; // 缓存命中,直接返回
    }
    
    // 2. 缓存没有,查数据库
    product = productMapper.selectById(id);
    
    // 3. 查到后写入缓存
    redisTemplate.opsForValue().set("product:" + id, product, 30, TimeUnit.MINUTES);
    
    return product;
}

优势:

  • 10万次请求 = 1次数据库查询(缓存未命中)+ 99999次缓存命中
  • 内存读取速度快1000倍
  • 数据库压力降低99%

四、Spring Boot整合Redis:手把手教你实现

4.1 第一步:添加依赖

<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Redis 依赖 -->
    <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>
    
    <!-- Lombok(简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

4.2 第二步:配置文件

# application.yml
server:
  port: 8080

spring:
  # Redis配置
  redis:
    host: localhost        # Redis服务器地址
    port: 6379             # Redis端口
    password:              # 密码(如果有)
    database: 0            # 数据库索引(0-15)
    timeout: 3000ms        # 连接超时时间
    lettuce:
      pool:
        max-active: 8      # 最大连接数
        max-wait: -1ms     # 最大等待时间
        max-idle: 8        # 最大空闲连接
        min-idle: 0        # 最小空闲连接

4.3 第三步:配置Redis序列化(重要!)

package com.example.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;

/**
 * Redis配置类
 * 解决Redis存储乱码问题
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // Key使用String序列化
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        
        // Value使用JSON序列化
        GenericJackson2JsonRedisSerializer jsonSerializer = 
            new GenericJackson2JsonRedisSerializer();
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
}

4.4 第四步:实体类

package com.example.redis.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;

/**
 * 商品实体类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
    
    private Long id;              // 商品ID
    private String name;          // 商品名称
    private BigDecimal price;     // 商品价格
    private Integer stock;        // 库存数量
    private String description;   // 商品描述
    private String imageUrl;      // 商品图片
}

4.5 第五步:商品服务类(核心代码)

package com.example.redis.service;

import com.example.redis.entity.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;

/**
 * 商品服务类
 * 演示Redis缓存的使用
 */
@Slf4j
@Service
public class ProductService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 模拟数据库存储
    private static final Product MOCK_PRODUCT = new Product(
        1L, "iPhone 15 Pro", new BigDecimal("7999"), 100, 
        "苹果最新旗舰手机", "https://example.com/iphone.jpg"
    );
    
    private static final String PRODUCT_KEY_PREFIX = "product:";
    private static final long CACHE_EXPIRE_TIME = 30; // 缓存30分钟

    /**
     * 获取商品信息(带缓存)
     * 先查Redis,没有再查数据库
     */
    public Product getProductById(Long id) {
        String key = PRODUCT_KEY_PREFIX + id;
        
        // 1. 先查Redis缓存
        Product product = (Product) redisTemplate.opsForValue().get(key);
        
        if (product != null) {
            log.info("✅ 缓存命中,商品信息从Redis获取:{}", product.getName());
            return product;
        }
        
        // 2. 缓存未命中,查询数据库(这里用模拟数据)
        log.info("❌ 缓存未命中,从数据库查询商品信息");
        product = MOCK_PRODUCT;
        
        // 3. 写入缓存,设置过期时间
        redisTemplate.opsForValue().set(key, product, CACHE_EXPIRE_TIME, TimeUnit.MINUTES);
        log.info("💾 商品信息已写入Redis缓存,过期时间:{}分钟", CACHE_EXPIRE_TIME);
        
        return product;
    }

    /**
     * 更新商品信息
     * 更新数据库后,同时删除缓存
     */
    public void updateProduct(Product product) {
        // 1. 更新数据库
        log.info("📝 更新数据库中的商品信息");
        // productMapper.updateById(product);
        
        // 2. 删除缓存(保证数据一致性)
        String key = PRODUCT_KEY_PREFIX + product.getId();
        redisTemplate.delete(key);
        log.info("🗑️ 已删除Redis缓存:{}", key);
    }

    /**
     * 删除商品
     * 删除数据库后,同时删除缓存
     */
    public void deleteProduct(Long id) {
        // 1. 删除数据库
        log.info("📝 删除数据库中的商品信息");
        // productMapper.deleteById(id);
        
        // 2. 删除缓存
        String key = PRODUCT_KEY_PREFIX + id;
        redisTemplate.delete(key);
        log.info("🗑️ 已删除Redis缓存:{}", key);
    }

    /**
     * 预热缓存
     * 系统启动时,将热点数据提前加载到缓存
     */
    public void warmUpCache(Long productId) {
        String key = PRODUCT_KEY_PREFIX + productId;
        Product product = MOCK_PRODUCT;
        redisTemplate.opsForValue().set(key, product, CACHE_EXPIRE_TIME, TimeUnit.MINUTES);
        log.info("🔥 缓存预热完成,商品ID:{}", productId);
    }
}

4.6 第六步:控制器类

package com.example.redis.controller;

import com.example.redis.entity.Product;
import com.example.redis.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 商品控制器
 */
@Slf4j
@RestController
@RequestMapping("/api/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 获取商品详情
     */
    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        return productService.getProductById(id);
    }

    /**
     * 更新商品
     */
    @PutMapping
    public String updateProduct(@RequestBody Product product) {
        productService.updateProduct(product);
        return "更新成功";
    }

    /**
     * 删除商品
     */
    @DeleteMapping("/{id}")
    public String deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
        return "删除成功";
    }

    /**
     * 缓存预热
     */
    @PostMapping("/warmup/{id}")
    public String warmUpCache(@PathVariable Long id) {
        productService.warmUpCache(id);
        return "缓存预热成功";
    }
}

4.7 第七步:启动类

package com.example.redis;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RedisApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisApplication.class, args);
    }
}

4.8 测试接口

# 1. 查询商品(第一次,缓存未命中)
curl http://localhost:8080/api/product/1

# 2. 查询商品(第二次,缓存命中)
curl http://localhost:8080/api/product/1

# 3. 更新商品(会删除缓存)
curl -X PUT http://localhost:8080/api/product \
  -H "Content-Type: application/json" \
  -d '{"id":1,"name":"iPhone 15 Pro Max","price":8999,"stock":50}'

# 4. 缓存预热
curl -X POST http://localhost:8080/api/product/warmup/1

五、实战踩坑:Redis缓存三大经典问题

5.1 缓存穿透

问题场景:

恶意用户大量查询不存在的商品ID(比如ID=-1),这些请求都会穿透Redis,直接打到数据库。

// ❌ 有问题的代码
public Product getProductById(Long id) {
    Product product = (Product) redisTemplate.opsForValue().get("product:" + id);
    if (product != null) {
        return product;
    }
    // 数据库查不到,返回null
    product = productMapper.selectById(id);
    return product;
}

解决方案:缓存空值

// ✅ 解决方案
public Product getProductById(Long id) {
    String key = "product:" + id;
    Product product = (Product) redisTemplate.opsForValue().get(key);
    
    if (product != null) {
        // 即使缓存的是null对象,也直接返回
        return product;
    }
    
    // 查询数据库
    product = productMapper.selectById(id);
    
    if (product == null) {
        // 数据库查不到,缓存一个空值,防止穿透
        redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
        return null;
    }
    
    // 查到了,缓存真实数据
    redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
    return product;
}

5.2 缓存雪崩

问题场景:

大量缓存的过期时间设置相同(都是30分钟),30分钟后这些缓存同时失效,导致大量请求瞬间打到数据库。

// ❌ 有问题的代码:所有缓存都是30分钟过期
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);

解决方案:随机过期时间

// ✅ 解决方案:过期时间加随机值
public Product getProductById(Long id) {
    String key = "product:" + id;
    Product product = (Product) redisTemplate.opsForValue().get(key);
    
    if (product != null) {
        return product;
    }
    
    product = productMapper.selectById(id);
    
    // 基础过期时间30分钟 + 随机0-10分钟
    long randomExpire = 30 + (long) (Math.random() * 10);
    redisTemplate.opsForValue().set(key, product, randomExpire, TimeUnit.MINUTES);
    
    return product;
}

5.3 缓存击穿

问题场景:

某个热点商品(比如iPhone)的缓存过期瞬间,大量请求同时查询这个商品,导致数据库瞬间压力激增。

解决方案:互斥锁

// ✅ 解决方案:使用Redis分布式锁
public Product getProductById(Long id) {
    String key = "product:" + id;
    String lockKey = "lock:product:" + id;
    
    // 1. 查询缓存
    Product product = (Product) redisTemplate.opsForValue().get(key);
    if (product != null) {
        return product;
    }
    
    // 2. 尝试获取锁
    Boolean lockAcquired = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    
    if (Boolean.TRUE.equals(lockAcquired)) {
        try {
            // 3. 获取到锁,再次检查缓存(双重检查)
            product = (Product) redisTemplate.opsForValue().get(key);
            if (product != null) {
                return product;
            }
            
            // 4. 查询数据库
            product = productMapper.selectById(id);
            
            // 5. 写入缓存
            redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
            
            return product;
        } finally {
            // 6. 释放锁
            redisTemplate.delete(lockKey);
        }
    } else {
        // 7. 没获取到锁,等待100ms后重试
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return getProductById(id); // 递归重试
    }
}

六、延伸阅读:进阶学习方向

掌握了Redis缓存基础后,你可以继续学习:

6.1 Redis进阶特性

  • Redis持久化:RDB和AOF的区别
  • Redis集群:主从复制、哨兵模式、Cluster集群
  • Redis事务:MULTI、EXEC、WATCH
  • Lua脚本:原子性操作

6.2 分布式锁

  • Redis分布式锁的实现原理
  • Redisson框架的使用
  • 看门狗机制

6.3 缓存设计模式

  • Cache-Aside:旁路缓存(本文讲的)
  • Read-Through:应用代码只读缓存
  • Write-Through:写缓存时同步写数据库
  • Write-Behind:异步写数据库

6.4 推荐学习资源

  • 官方文档:https://redis.io/docs/
  • 书籍:《Redis实战》、《Redis设计与实现》
  • 视频教程:B站搜索"Redis教程"

七、总结

今天我们从电商秒杀场景出发,学习了:

  1. 什么是Redis:内存数据库,速度快1000倍
  2. 为什么用Redis:减轻数据库压力,提升系统性能
  3. 怎么用Redis:Spring Boot集成Redis的完整代码示例
  4. 常见问题:缓存穿透、缓存雪崩、缓存击穿的解决方案

记住一句话:Redis就像系统的"外挂内存",用好了能让你的系统性能提升10倍!

💡 最后的小建议

  • 不要为了用Redis而用Redis,先分析你的系统是否真的需要
  • 缓存不是银弹,要考虑数据一致性问题
  • 多看日志,多监控,及时发现问题

如果这篇文章对你有帮助,记得点赞收藏哦!有问题欢迎在评论区讨论~ 🎉


作者简介:一名热爱分享的Java开发者,专注后端技术,喜欢用大白话讲解复杂技术。关注我,一起学习进步!

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐