高并发解决方案:SpringBoot+Redis分布式缓存实战

博主介绍:Java、Python、js全栈开发 “多面手”,精通多种编程语言和技术,痴迷于人工智能领域。秉持着对技术的热爱与执着,持续探索创新,愿在此分享交流和学习,与大家共进步。
DeepSeek-行业融合之万象视界(附实战案例详解100+)
全栈开发环境搭建运行攻略:多语言一站式指南(环境搭建+运行+调试+发布+保姆级详解)
感兴趣的可以先收藏起来,希望帮助更多的人
在这里插入图片描述

高并发解决方案:SpringBoot + Redis 分布式缓存实战

一、引言

在当今互联网时代,高并发场景无处不在,如电商平台的秒杀活动、社交媒体的热点事件等。系统面临高并发请求时,若处理不当,可能会导致响应缓慢、系统崩溃等问题。为了应对这些挑战,分布式缓存技术应运而生。Redis 作为一款高性能的内存数据库,因其快速读写、丰富的数据结构等特性,成为了分布式缓存的首选。本文将详细介绍如何使用 Spring Boot 结合 Redis 构建分布式缓存系统,以解决高并发问题。

二、高并发问题概述

2.1 高并发场景特点

高并发场景通常具有以下特点:

  • 大量请求:在短时间内会有大量的用户请求涌入系统,如秒杀活动开始瞬间,可能会有数十万甚至数百万的请求。
  • 数据一致性要求高:在高并发场景下,数据的一致性至关重要,例如库存数据的准确性,若出现数据不一致,可能会导致超卖等问题。
  • 性能要求高:用户期望系统能够快速响应请求,若响应时间过长,会影响用户体验,甚至导致用户流失。

2.2 高并发带来的问题

  • 数据库压力过大:大量的请求直接访问数据库,会导致数据库的负载过高,性能下降,甚至出现崩溃的情况。
  • 响应时间变长:由于数据库处理能力有限,在高并发情况下,请求的处理时间会变长,导致用户需要等待较长时间才能得到响应。
  • 系统可用性降低:当系统无法承受高并发请求时,可能会出现服务不可用的情况,影响业务的正常运行。

三、Redis 简介

3.1 Redis 基本概念

Redis(Remote Dictionary Server)是一个开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。Redis 支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)等。

3.2 Redis 作为缓存的优势

  • 高性能:Redis 数据存储在内存中,读写速度非常快,能够显著提高系统的响应性能。
  • 丰富的数据结构:Redis 支持多种数据结构,可以根据不同的业务场景选择合适的数据结构进行缓存,提高缓存的灵活性。
  • 分布式支持:Redis 可以通过集群、哨兵等方式实现分布式部署,提高系统的可扩展性和可用性。
  • 持久化:Redis 支持数据持久化,即使服务器重启,数据也不会丢失,保证了数据的安全性。

四、Spring Boot 集成 Redis

4.1 项目搭建

首先,我们需要创建一个 Spring Boot 项目。可以使用 Spring Initializr(https://start.spring.io/)来快速创建项目,添加以下依赖:

<dependencies>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
    <dependency>
        <groupId>redis.clientsgroupId>
        <artifactId>jedisartifactId>
    dependency>
dependencies>

4.2 配置 Redis

application.propertiesapplication.yml 中配置 Redis 连接信息:

spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=

4.3 创建 Redis 配置类

创建一个 Redis 配置类,用于配置 RedisTemplate:

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 redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 设置 key 的序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 设置 value 的序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        template.afterPropertiesSet();
        return template;
    }
}

4.4 测试 Redis 连接

创建一个简单的测试类,验证 Redis 连接是否正常:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RedisTestController {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/testRedis")
    public String testRedis() {
        redisTemplate.opsForValue().set("testKey", "testValue");
        Object value = redisTemplate.opsForValue().get("testKey");
        return value.toString();
    }
}

启动 Spring Boot 应用,访问 http://localhost:8080/testRedis,若返回 testValue,则说明 Redis 连接正常。

五、使用 Redis 缓存解决高并发问题

5.1 缓存穿透问题及解决方案

5.1.1 缓存穿透问题描述

缓存穿透是指大量请求的 key 既不在缓存中,也不在数据库中,导致请求直接穿透缓存访问数据库,从而给数据库带来巨大压力。例如,恶意用户可能会使用不存在的 key 进行大量请求,攻击系统。

5.1.2 解决方案
  • 布隆过滤器:布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否存在于一个集合中。在请求进入系统时,先通过布隆过滤器判断 key 是否存在,如果不存在,则直接返回,避免访问数据库。
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.nio.charset.Charset;

@Component
public class BloomFilterConfig {

    private BloomFilter<String> bloomFilter;

    @PostConstruct
    public void init() {
        // 预计插入数量
        int expectedInsertions = 1000000;
        // 误判率
        double fpp = 0.001;
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), expectedInsertions, fpp);
        // 初始化布隆过滤器数据
        // 这里可以从数据库中读取所有有效的 key 并添加到布隆过滤器中
        // bloomFilter.put("key1");
        // bloomFilter.put("key2");
    }

    public boolean mightContain(String key) {
        return bloomFilter.mightContain(key);
    }
}

在 Controller 中使用布隆过滤器:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BloomFilterController {

    @Autowired
    private BloomFilterConfig bloomFilterConfig;

    @GetMapping("/bloomFilter/{key}")
    public String bloomFilterTest(@PathVariable String key) {
        if (!bloomFilterConfig.mightContain(key)) {
            return "Key not found";
        }
        // 继续处理请求
        return "Processing...";
    }
}
  • 缓存空对象:当请求的 key 不存在于数据库中时,也将该 key 对应的空对象缓存起来,并设置一个较短的过期时间,这样下次相同的请求就可以直接从缓存中获取空对象,避免再次访问数据库。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class CacheService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public Object getData(String key) {
        Object value = redisTemplate.opsForValue().get(key);
        if (value == null) {
            // 模拟从数据库中查询数据
            Object dataFromDB = queryDataFromDB(key);
            if (dataFromDB == null) {
                // 缓存空对象,设置较短的过期时间
                redisTemplate.opsForValue().set(key, "", 1, TimeUnit.MINUTES);
            } else {
                redisTemplate.opsForValue().set(key, dataFromDB);
            }
            return dataFromDB;
        }
        return value;
    }

    private Object queryDataFromDB(String key) {
        // 模拟从数据库中查询数据
        return null;
    }
}

5.2 缓存雪崩问题及解决方案

5.2.1 缓存雪崩问题描述

缓存雪崩是指在某一时刻,大量的缓存 key 同时过期,导致大量请求直接访问数据库,从而使数据库压力过大,甚至崩溃。例如,系统在凌晨进行缓存更新,将大量的缓存 key 设置了相同的过期时间,当这些 key 同时过期时,就会引发缓存雪崩。

5.2.2 解决方案
  • 设置随机过期时间:为每个缓存 key 设置一个随机的过期时间,避免大量 key 同时过期。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Random;
import java.util.concurrent.TimeUnit;

@Service
public class CacheServiceWithRandomExpire {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void setDataWithRandomExpire(String key, Object value) {
        Random random = new Random();
        // 随机生成 1 到 60 分钟的过期时间
        long expireTime = random.nextInt(60) + 1;
        redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.MINUTES);
    }
}
  • 使用多级缓存:除了 Redis 缓存外,还可以使用本地缓存(如 Guava Cache)作为一级缓存,Redis 作为二级缓存。当本地缓存中没有数据时,再从 Redis 中获取数据,这样可以减少对 Redis 的访问压力,同时也能提高系统的响应速度。
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class MultiLevelCacheService {

    private Cache<String, Object> localCache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public Object getData(String key) {
        // 先从本地缓存中获取数据
        Object value = localCache.getIfPresent(key);
        if (value == null) {
            // 本地缓存中没有数据,从 Redis 中获取
            value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                // 将数据放入本地缓存
                localCache.put(key, value);
            }
        }
        return value;
    }
}

5.3 缓存击穿问题及解决方案

5.3.1 缓存击穿问题描述

缓存击穿是指某个热点 key 在缓存中过期时,恰好有大量的请求同时访问该 key,导致这些请求直接访问数据库,从而使数据库压力过大。例如,在电商平台的秒杀活动中,某个热门商品的缓存 key 过期时,可能会有大量用户同时请求该商品信息,引发缓存击穿问题。

5.3.2 解决方案
  • 互斥锁:在访问数据库之前,先获取一个互斥锁,只有获取到锁的请求才能访问数据库,其他请求需要等待。当获取到锁的请求将数据更新到缓存后,其他请求可以直接从缓存中获取数据。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

@Service
public class CacheServiceWithMutex {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String LOCK_KEY = "cache_mutex:";
    private static final String LOCK_VALUE = "lock";
    private static final long LOCK_EXPIRE_TIME = 10;

    public Object getData(String key) {
        Object value = redisTemplate.opsForValue().get(key);
        if (value == null) {
            // 获取锁
            boolean locked = tryLock(LOCK_KEY + key);
            if (locked) {
                try {
                    // 再次检查缓存
                    value = redisTemplate.opsForValue().get(key);
                    if (value == null) {
                        // 从数据库中查询数据
                        value = queryDataFromDB(key);
                        if (value != null) {
                            // 将数据放入缓存
                            redisTemplate.opsForValue().set(key, value);
                        }
                    }
                } finally {
                    // 释放锁
                    releaseLock(LOCK_KEY + key);
                }
            } else {
                // 等待一段时间后重试
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return getData(key);
            }
        }
        return value;
    }

    private boolean tryLock(String key) {
        RedisScript<Long> script = new DefaultRedisScript<>("if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end", Long.class);
        Long result = redisTemplate.execute(script, Collections.singletonList(key), LOCK_VALUE, LOCK_EXPIRE_TIME);
        return result != null && result == 1;
    }

    private void releaseLock(String key) {
        RedisScript<Long> script = new DefaultRedisScript<>("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class);
        redisTemplate.execute(script, Collections.singletonList(key), LOCK_VALUE);
    }

    private Object queryDataFromDB(String key) {
        // 模拟从数据库中查询数据
        return null;
    }
}
  • 永不过期:对于热点 key,不设置过期时间,而是通过定时任务或其他方式来更新缓存数据,这样可以避免缓存击穿问题。

六、总结

通过本文的介绍,我们了解了高并发场景下可能出现的问题,以及如何使用 Spring Boot 结合 Redis 构建分布式缓存系统来解决这些问题。我们详细介绍了 Redis 的基本概念和优势,以及 Spring Boot 集成 Redis 的步骤。同时,针对缓存穿透、缓存雪崩和缓存击穿等问题,给出了相应的解决方案和代码示例。在实际开发中,我们可以根据具体的业务场景选择合适的解决方案,以提高系统的性能和可用性。

你可能感兴趣的:(Web,缓存,spring,boot,redis)