Java 项目中 Redis 的应用场景与实战示例
一篇面向 Java 后端工程师的 Redis 应用全景指南:涵盖核心场景、完整代码、流程图与最佳实践。
一、前言
在现代 Java 后端项目中,Redis 几乎是不可或缺的基础组件。它不仅是一个高性能的内存数据库,更是一把"瑞士军刀"——可以做缓存、分布式锁、限流器、消息队列、排行榜、会话存储等等。
本文将结合 Spring Boot + Spring Data Redis + Redisson 技术栈,系统梳理 Redis 在 Java 项目中最常见的 8 大应用场景,并提供完整可运行的示例代码和流程图,帮助你在真实项目中落地。
二、Redis 在 Java 项目中的整体定位
在典型的 Spring Boot 微服务架构中,Redis 通常处于应用层和数据库层之间,承担"加速"和"解耦"两大职责。
Redis 的核心价值可概括为三点:
- 快:基于内存 + 单线程 + IO 多路复用,单机 QPS 可达 10 万+。
- 多:支持 String、Hash、List、Set、ZSet、Stream、BitMap、HyperLogLog、GEO 等多种数据结构。
- 稳:支持持久化(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"(旁路缓存)流程如下:
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);
}
}
五、场景二:分布式锁
在集群环境下,synchronized 和 ReentrantLock 只能锁住单 JVM,需要 Redis 提供跨进程的互斥能力。
5.1 分布式锁流程图
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 存储"到期时间戳",后台线程轮询即可实现延时队列。适用于订单超时关闭、延时通知等场景。
@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;
}
}
十二、生产环境最佳实践
-
Key 规范:采用
业务:实体:id风格(如user:profile:1001),便于维护与监控。 -
必加 TTL:除少数配置类数据外,所有 key 都应设置过期时间,避免内存无限增长。
-
避免大 Key:单个 String 建议 < 10KB,单个 Hash/List/ZSet 元素数 < 5000;大 Key 使用
HSCAN/SCAN分批处理。 -
慎用 KEYS/FLUSHALL:生产环境禁用,改用
SCAN遍历。 -
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; }); -
缓存与 DB 双写一致性:推荐"先更新 DB,再删除缓存";强一致场景结合 Canal 订阅 binlog 异步补偿。
-
监控告警:关注
used_memory、connected_clients、instantaneous_ops_per_sec、慢查询日志;可接入 Prometheus + Grafana。 -
高可用:单机版仅用于开发;生产最少使用 Sentry 主从切换或 Redis Cluster。
十三、总结
本文以 Spring Boot + Redis 为技术栈,完整梳理了 Redis 在 Java 项目中最主流的 8 大应用场景:
| 场景 | 核心数据结构 | 典型命令 | 推荐方案 |
|---|---|---|---|
| 缓存 | String / Hash | SET / GET | Spring Cache + TTL 随机化 |
| 分布式锁 | String | SET NX EX + Lua | Redisson |
| 限流 | ZSet / String | Lua 脚本 | 滑动窗口 + AOP |
| 会话共享 | Hash | - | Spring Session |
| 排行榜 | ZSet | ZADD / ZREVRANGE | incrementScore |
| 计数器 | String / Hash | INCR / HINCRBY | 日粒度 Hash |
| 消息队列 | Stream / List | XADD / XREADGROUP | Stream + 消费组 |
| 延时队列 | ZSet | ZADD / ZRANGEBYSCORE | 定时扫描 |
| BitMap 统计 | String | SETBIT / BITCOUNT | 签到/活跃 |
| 地理位置 | GEO | GEOADD / GEOSEARCH | 附近商家 |
一句话总结:Redis 不是银弹,但它几乎是 Java 后端性能优化的"第一站"。掌握上述场景与代码,你就能从容应对 90% 的高并发需求。
希望本文对你有所帮助。如有疑问或更好的实践,欢迎在评论区交流。