Redis可不仅仅是一个简单的KV缓存工具,在Java项目里还有八大场景妙用。。。

0 阅读9分钟

Java 项目中 Redis 的应用场景与实战示例

一篇面向 Java 后端工程师的 Redis 应用全景指南:涵盖核心场景、完整代码、流程图与最佳实践。


一、前言

在现代 Java 后端项目中,Redis 几乎是不可或缺的基础组件。它不仅是一个高性能的内存数据库,更是一把"瑞士军刀"——可以做缓存、分布式锁、限流器、消息队列、排行榜、会话存储等等。

本文将结合 Spring Boot + Spring Data Redis + Redisson 技术栈,系统梳理 Redis 在 Java 项目中最常见的 8 大应用场景,并提供完整可运行的示例代码和流程图,帮助你在真实项目中落地。


二、Redis 在 Java 项目中的整体定位

在典型的 Spring Boot 微服务架构中,Redis 通常处于应用层和数据库层之间,承担"加速"和"解耦"两大职责。

image.png

Redis 的核心价值可概括为三点:

  1. :基于内存 + 单线程 + IO 多路复用,单机 QPS 可达 10 万+。
  2. :支持 String、Hash、List、Set、ZSet、Stream、BitMap、HyperLogLog、GEO 等多种数据结构。
  3. :支持持久化(RDB/AOF)、主从复制、哨兵、Cluster 集群,生产级可用。

三、环境准备与依赖引入

3.1 引入 Spring Data Redis 依赖

以 Maven 为例,在 pom.xml 中加入:

<dependencies>
    <!-- Spring Data Redis (底层默认使用 Lettuce) -->
    <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>
​
    <!-- Redisson(分布式锁等高级功能) -->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.27.2</version>
    </dependency>
​
    <!-- JSON 序列化 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

3.2 application.yml 配置

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 123456
      database: 0
      timeout: 3000ms
      lettuce:
        pool:
          max-active: 32
          max-idle: 16
          min-idle: 4
          max-wait: 2000ms

3.3 RedisTemplate 序列化配置

默认的 JdkSerializer 序列化结果不可读且无法跨语言。生产环境推荐使用 Jackson 序列化:

package com.example.redis.config;
​
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
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.Jackson2JsonRedisSerializer;
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);
​
        Jackson2JsonRedisSerializer<Object> jsonSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL);
        jsonSerializer.setObjectMapper(mapper);
​
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

四、场景一:缓存(Cache)

缓存是 Redis 最经典的应用场景,用于减少数据库压力、提升接口响应速度。

4.1 缓存读写流程图

标准的"Cache Aside Pattern"(旁路缓存)流程如下:

image.png

4.2 手动缓存示例

package com.example.redis.service;
​
import com.example.redis.entity.Product;
import com.example.redis.mapper.ProductMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
​
import java.util.concurrent.TimeUnit;
​
@Service
@RequiredArgsConstructor
public class ProductService {
​
    private final RedisTemplate<String, Object> redisTemplate;
    private final ProductMapper productMapper;
​
    private static final String CACHE_KEY = "product:";
    private static final long CACHE_TTL = 10;
​
    public Product getById(Long id) {
        String key = CACHE_KEY + id;
​
        Product cached = (Product) redisTemplate.opsForValue().get(key);
        if (cached != null) {
            return cached;
        }
​
        Product product = productMapper.selectById(id);
        if (product != null) {
            redisTemplate.opsForValue().set(key, product, CACHE_TTL, TimeUnit.MINUTES);
        }
        return product;
    }
​
    public void update(Product product) {
        productMapper.updateById(product);
        redisTemplate.delete(CACHE_KEY + product.getId());
    }
}

4.3 Spring Cache 注解式示例

使用注解能让代码更简洁:

@EnableCaching
@Configuration
public class CacheConfig {
​
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues();
        return RedisCacheManager.builder(factory).cacheDefaults(config).build();
    }
}
@Service
public class ProductCacheService {
​
    @Cacheable(value = "product", key = "#id", unless = "#result == null")
    public Product getById(Long id) {
        return productMapper.selectById(id);
    }
​
    @CachePut(value = "product", key = "#product.id")
    public Product update(Product product) {
        productMapper.updateById(product);
        return product;
    }
​
    @CacheEvict(value = "product", key = "#id")
    public void delete(Long id) {
        productMapper.deleteById(id);
    }
}

4.4 缓存穿透、击穿、雪崩的应对

问题现象解决方案
穿透查询不存在的 key,打穿到 DB缓存空值 + 布隆过滤器
击穿热 key 过期瞬间大量请求打 DB互斥锁 + 逻辑过期
雪崩大量 key 同时过期TTL 加随机值 + 多级缓存

下面是一个综合处理"穿透 + 击穿"的示例:

public Product getByIdSafely(Long id) {
    String key = CACHE_KEY + id;

    Object cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
        if (cached instanceof String && "NULL".equals(cached)) {
            return null;
        }
        return (Product) cached;
    }

    String lockKey = "lock:product:" + id;
    Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    try {
        if (Boolean.FALSE.equals(locked)) {
            Thread.sleep(50);
            return getByIdSafely(id);
        }

        Product product = productMapper.selectById(id);
        if (product == null) {
            redisTemplate.opsForValue().set(key, "NULL", 2, TimeUnit.MINUTES);
            return null;
        }
        long ttl = 10 + ThreadLocalRandom.current().nextInt(5);
        redisTemplate.opsForValue().set(key, product, ttl, TimeUnit.MINUTES);
        return product;
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return null;
    } finally {
        redisTemplate.delete(lockKey);
    }
}

五、场景二:分布式锁

在集群环境下,synchronizedReentrantLock 只能锁住单 JVM,需要 Redis 提供跨进程的互斥能力。

5.1 分布式锁流程图

image.png

5.2 基于 SETNX 的简易锁

@Component
public class SimpleRedisLock {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final String UNLOCK_LUA =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else return 0 end";

    public String tryLock(String key, long expireSeconds) {
        String value = UUID.randomUUID().toString();
        Boolean ok = stringRedisTemplate.opsForValue()
                .setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(ok) ? value : null;
    }

    public boolean unlock(String key, String value) {
        Long result = stringRedisTemplate.execute(
                new DefaultRedisScript<>(UNLOCK_LUA, Long.class),
                Collections.singletonList(key),
                value);
        return result != null && result > 0;
    }
}

要点

  • 加锁要保证"原子性" → SET key value NX EX seconds
  • 释放锁要校验持有者 → 使用 Lua 保证"判断+删除"的原子性
  • value 使用 UUID,避免误删他人锁

5.3 基于 Redisson 的可重入锁

Redisson 更适合生产环境,内置看门狗续期、可重入、公平锁、红锁等高级特性。

@Service
@RequiredArgsConstructor
public class OrderService {

    private final RedissonClient redissonClient;

    public void createOrder(Long userId, Long productId) {
        String lockKey = "lock:stock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            boolean ok = lock.tryLock(3, 30, TimeUnit.SECONDS);
            if (!ok) {
                throw new RuntimeException("抢购火爆,请稍后再试");
            }
            deductStock(productId);
            saveOrder(userId, productId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

六、场景三:分布式限流

在秒杀、开放 API、防刷等场景中,需要对接口做 QPS 限制。Redis + Lua 是实现分布式限流的经典方案。

6.1 基于 Lua 脚本的滑动窗口限流

rate_limit.lua

-- KEYS[1]: 限流 key
-- ARGV[1]: 窗口大小(毫秒)
-- ARGV[2]: 最大请求数
-- ARGV[3]: 当前时间戳(毫秒)
local key = KEYS[1]
local window = tonumber(ARGV[1])
local maxCount = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local current = redis.call('ZCARD', key)
if current < maxCount then
    redis.call('ZADD', key, now, now)
    redis.call('PEXPIRE', key, window)
    return 1
else
    return 0
end

Java 调用:

@Component
public class RateLimiter {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private final DefaultRedisScript<Long> script;

    public RateLimiter() {
        script = new DefaultRedisScript<>();
        script.setLocation(new ClassPathResource("scripts/rate_limit.lua"));
        script.setResultType(Long.class);
    }

    public boolean tryAcquire(String key, int windowMs, int maxCount) {
        Long result = stringRedisTemplate.execute(
                script,
                Collections.singletonList(key),
                String.valueOf(windowMs),
                String.valueOf(maxCount),
                String.valueOf(System.currentTimeMillis()));
        return result != null && result == 1L;
    }
}

6.2 注解式限流

定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    String key() default "";
    int window() default 1000;
    int max() default 10;
}

AOP 拦截:

@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {

    private final RateLimiter rateLimiter;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
        String key = "rl:" + (rateLimit.key().isEmpty()
                ? pjp.getSignature().toShortString()
                : rateLimit.key());
        if (!rateLimiter.tryAcquire(key, rateLimit.window(), rateLimit.max())) {
            throw new RuntimeException("请求过于频繁,请稍后再试");
        }
        return pjp.proceed();
    }
}

使用:

@RestController
public class OrderController {

    @RateLimit(key = "order:create", window = 1000, max = 100)
    @PostMapping("/order")
    public String create() {
        return "ok";
    }
}

七、场景四:分布式会话(Session 共享)

传统 Tomcat Session 只存在单节点内存中,微服务/多实例部署下会出现 "登录丢失"。使用 Spring Session + Redis 可以轻松实现 Session 共享。

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
spring:
  session:
    store-type: redis
    timeout: 30m
    redis:
      namespace: spring:session
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
}

启动后,任何 HttpSession 数据都会自动写入 Redis,多个实例间共享。

八、场景五:排行榜与计数器

8.1 使用 ZSet 实现实时排行榜

ZSet 内部基于跳表 + 哈希表,天然适合"按分数排序"的场景。

@Service
@RequiredArgsConstructor
public class RankService {

    private final StringRedisTemplate redisTemplate;
    private static final String RANK_KEY = "rank:game:daily";

    public void addScore(String userId, double score) {
        redisTemplate.opsForZSet().incrementScore(RANK_KEY, userId, score);
    }

    public List<RankItem> top(int n) {
        Set<ZSetOperations.TypedTuple<String>> tuples =
                redisTemplate.opsForZSet().reverseRangeWithScores(RANK_KEY, 0, n - 1);
        if (tuples == null) return Collections.emptyList();
        List<RankItem> list = new ArrayList<>();
        int rank = 1;
        for (ZSetOperations.TypedTuple<String> t : tuples) {
            list.add(new RankItem(rank++, t.getValue(), t.getScore()));
        }
        return list;
    }

    public Long myRank(String userId) {
        Long rank = redisTemplate.opsForZSet().reverseRank(RANK_KEY, userId);
        return rank == null ? -1L : rank + 1;
    }
}

8.2 使用 INCR 实现计数器

文章阅读量、点赞数、PV/UV 等:

public long incrView(Long articleId) {
    String key = "article:view:" + articleId;
    return redisTemplate.opsForValue().increment(key);
}

public long incrViewWithExpire(Long articleId) {
    String key = "article:view:daily:" + LocalDate.now();
    Long count = redisTemplate.opsForHash().increment(key, articleId.toString(), 1);
    redisTemplate.expire(key, 2, TimeUnit.DAYS);
    return count;
}

九、场景六:消息队列与延时队列

9.1 基于 Stream 的消息队列

Redis 5.0+ 推出的 Stream 类型,支持消费组、ACK、持久化,是轻量级 MQ 的理想选择。

生产者:

@Service
@RequiredArgsConstructor
public class OrderProducer {

    private final StringRedisTemplate redisTemplate;

    public void sendOrder(String orderId) {
        Map<String, String> msg = Map.of(
                "orderId", orderId,
                "time", String.valueOf(System.currentTimeMillis()));
        RecordId id = redisTemplate.opsForStream()
                .add("stream:order", msg);
        System.out.println("消息发送成功: " + id);
    }
}

消费者:

@Component
public class OrderConsumer implements StreamListener<String, MapRecord<String, String, String>> {

    @Resource
    private StringRedisTemplate redisTemplate;

    @PostConstruct
    public void init() {
        try {
            redisTemplate.opsForStream().createGroup("stream:order", "order-group");
        } catch (Exception ignored) {
        }

        StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
                StreamMessageListenerContainer.StreamMessageListenerContainerOptions
                        .builder()
                        .pollTimeout(Duration.ofSeconds(2))
                        .build();

        StreamMessageListenerContainer<String, MapRecord<String, String, String>> container =
                StreamMessageListenerContainer.create(
                        redisTemplate.getConnectionFactory(), options);

        container.receive(
                Consumer.from("order-group", "consumer-1"),
                StreamOffset.create("stream:order", ReadOffset.lastConsumed()),
                this);
        container.start();
    }

    @Override
    public void onMessage(MapRecord<String, String, String> msg) {
        System.out.println("收到订单消息: " + msg.getValue());
        redisTemplate.opsForStream()
                .acknowledge("order-group", msg);
    }
}

9.2 基于 ZSet 的延时队列

利用 ZSet 的 score 存储"到期时间戳",后台线程轮询即可实现延时队列。适用于订单超时关闭、延时通知等场景。

image.png

@Service
@RequiredArgsConstructor
@Slf4j
public class DelayQueueService {

    private final StringRedisTemplate redisTemplate;
    private static final String QUEUE_KEY = "delay:order:close";

    public void push(String orderId, long delayMs) {
        long triggerTime = System.currentTimeMillis() + delayMs;
        redisTemplate.opsForZSet().add(QUEUE_KEY, orderId, triggerTime);
    }

    @Scheduled(fixedDelay = 1000)
    public void scan() {
        long now = System.currentTimeMillis();
        Set<String> dueItems = redisTemplate.opsForZSet()
                .rangeByScore(QUEUE_KEY, 0, now, 0, 50);
        if (dueItems == null || dueItems.isEmpty()) return;
        for (String orderId : dueItems) {
            Long removed = redisTemplate.opsForZSet().remove(QUEUE_KEY, orderId);
            if (removed != null && removed > 0) {
                log.info("关闭超时订单: {}", orderId);
            }
        }
    }
}

十、场景七:位图(BitMap)统计

BitMap 可以用极小的空间统计亿级用户的"是/否"状态,例如签到、活跃用户数等。

@Service
@RequiredArgsConstructor
public class SignService {

    private final StringRedisTemplate redisTemplate;

    public void sign(Long userId) {
        String key = "sign:" + userId + ":" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
        int day = LocalDate.now().getDayOfMonth() - 1;
        redisTemplate.opsForValue().setBit(key, day, true);
    }

    public boolean isSigned(Long userId, LocalDate date) {
        String key = "sign:" + userId + ":" + date.format(DateTimeFormatter.ofPattern("yyyyMM"));
        int day = date.getDayOfMonth() - 1;
        Boolean bit = redisTemplate.opsForValue().getBit(key, day);
        return Boolean.TRUE.equals(bit);
    }

    public long signDaysOfMonth(Long userId) {
        String key = "sign:" + userId + ":" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
        Long count = redisTemplate.execute(
                (RedisCallback<Long>) conn -> conn.bitCount(key.getBytes()));
        return count == null ? 0 : count;
    }
}

1000 万用户一个月的签到数据只需约 30MB,空间效率极高。


十一、场景八:GEO 地理位置

Redis 的 GEO 类型可以实现"附近的人"、"附近商家"等 LBS 功能。

@Service
@RequiredArgsConstructor
public class GeoService {

    private final StringRedisTemplate redisTemplate;
    private static final String KEY = "geo:shop";

    public void addShop(String shopId, double lng, double lat) {
        redisTemplate.opsForGeo().add(KEY, new Point(lng, lat), shopId);
    }

    public List<NearbyShop> nearby(double lng, double lat, double radiusKm, int limit) {
        Circle circle = new Circle(new Point(lng, lat), new Distance(radiusKm, Metrics.KILOMETERS));
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
                .newGeoRadiusArgs()
                .includeCoordinates()
                .includeDistance()
                .sortAscending()
                .limit(limit);

        GeoResults<RedisGeoCommands.GeoLocation<String>> results =
                redisTemplate.opsForGeo().radius(KEY, circle, args);

        if (results == null) return Collections.emptyList();

        List<NearbyShop> list = new ArrayList<>();
        for (GeoResult<RedisGeoCommands.GeoLocation<String>> r : results) {
            RedisGeoCommands.GeoLocation<String> loc = r.getContent();
            list.add(new NearbyShop(
                    loc.getName(),
                    loc.getPoint().getX(),
                    loc.getPoint().getY(),
                    r.getDistance().getValue()));
        }
        return list;
    }
}

十二、生产环境最佳实践

  1. Key 规范:采用 业务:实体:id 风格(如 user:profile:1001),便于维护与监控。

  2. 必加 TTL:除少数配置类数据外,所有 key 都应设置过期时间,避免内存无限增长。

  3. 避免大 Key:单个 String 建议 < 10KB,单个 Hash/List/ZSet 元素数 < 5000;大 Key 使用 HSCAN/SCAN 分批处理。

  4. 慎用 KEYS/FLUSHALL:生产环境禁用,改用 SCAN 遍历。

  5. Pipeline 批处理:批量写入时使用 pipelined,减少 RTT:

    redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
        for (int i = 0; i < 1000; i++) {
            conn.stringCommands().set(("k" + i).getBytes(), ("v" + i).getBytes());
        }
        return null;
    });
    
  6. 缓存与 DB 双写一致性:推荐"先更新 DB,再删除缓存";强一致场景结合 Canal 订阅 binlog 异步补偿。

  7. 监控告警:关注 used_memoryconnected_clientsinstantaneous_ops_per_sec、慢查询日志;可接入 Prometheus + Grafana。

  8. 高可用:单机版仅用于开发;生产最少使用 Sentry 主从切换或 Redis Cluster。

image.png


十三、总结

本文以 Spring Boot + Redis 为技术栈,完整梳理了 Redis 在 Java 项目中最主流的 8 大应用场景

场景核心数据结构典型命令推荐方案
缓存String / HashSET / GETSpring Cache + TTL 随机化
分布式锁StringSET NX EX + LuaRedisson
限流ZSet / StringLua 脚本滑动窗口 + AOP
会话共享Hash-Spring Session
排行榜ZSetZADD / ZREVRANGEincrementScore
计数器String / HashINCR / HINCRBY日粒度 Hash
消息队列Stream / ListXADD / XREADGROUPStream + 消费组
延时队列ZSetZADD / ZRANGEBYSCORE定时扫描
BitMap 统计StringSETBIT / BITCOUNT签到/活跃
地理位置GEOGEOADD / GEOSEARCH附近商家

一句话总结:Redis 不是银弹,但它几乎是 Java 后端性能优化的"第一站"。掌握上述场景与代码,你就能从容应对 90% 的高并发需求。

希望本文对你有所帮助。如有疑问或更好的实践,欢迎在评论区交流。