一、 核心配置:序列化与连接池
在开始特殊用法前,必须确保 RedisTemplate 配置正确,避免出现乱码或性能瓶颈。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用 JSON 序列化保存 Value,方便观察数据
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jsonSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jsonSerializer);
return template;
}
}
二、 事务 (Transactions)
Redis 事务通过 MULTI, EXEC, DISCARD 命令实现。它保证了一组命令的原子性(要么都发给服务端,要么都不发),但注意 Redis 事务不支持回滚。
实战代码:
public void executeTransaction() {
redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.watch("balance"); // 乐观锁:监视 key 是否被改动
operations.multi();
operations.opsForValue().increment("balance", -100);
operations.opsForValue().increment("score", 100);
return operations.exec(); // 提交事务
}
});
}
三、 流水线 (Pipelining)
为什么要用? 当你需要连续执行 1000 条命令时,往返的网络耗时(RTT)远大于 Redis 处理的时间。Pipeline 可以将命令一次性打包发送,性能提升巨大。
实战代码:
public void usePipelined() {
List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (int i = 0; i < 1000; i++) {
connection.set(("key_" + i).getBytes(), "value".getBytes());
}
return null; // 必须返回 null
});
}
四、 发布/订阅 (Pub/Sub)
Redis 的发布订阅是一种轻量级的消息队列,适用于实时性要求高但不需要持久化的场景。
1. 订阅者(配置):
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory factory,
MessageListenerAdapter adapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener(adapter, new PatternTopic("chat_room"));
return container;
}
2. 发布者代码:
public void publishMessage(String msg) {
redisTemplate.convertAndSend("chat_room", msg);
}
五、 Lua 脚本 (Scripting)
Lua 脚本是 Redis 处理复杂逻辑的终极武器,它能保证 多条命令的绝对原子性(脚本执行期间其他命令会被阻塞)。
典型场景:分布式限流
public boolean tryLock(String key, String value, int expireTime) {
String script = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
"return redis.call('expire', KEYS[1], ARGV[2]) " +
"else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value, expireTime);
return result != null && result.equals(1L);
}
六、 布隆过滤器 (Bloom Filter)
布隆过滤器用于判断“某个值是否一定不存在”。它内存占用极小,常用于解决缓存穿透。
注意:原生 Redis 不带布隆过滤器,需安装 RedisBloom 模块。Spring Data Redis 通过
execute调用原生命令。
实战代码:
public void bloomOps() {
// 1. 添加元素
redisTemplate.execute((RedisCallback<Object>) conn ->
conn.execute("BF.ADD", "user_exists".getBytes(), "user_101".getBytes()));
// 2. 检查是否存在
Boolean exists = (Boolean) redisTemplate.execute((RedisCallback<Object>) conn ->
conn.execute("BF.EXISTS", "user_exists".getBytes(), "user_101".getBytes()));
}
七、 Stream:持久化消息队列 (Redis 5.0+)
为什么要用? 发布/订阅(Pub/Sub)不能持久化消息。Stream 借鉴了 Kafka,支持消息持久化、消费组(Consumer Group)和消息确认(ACK) 。
实战代码:发送与消费
public void streamOps() {
// 1. 发送消息
ObjectRecord<String, String> record = StreamRecords.newRecord()
.in("order_stream")
.ofObject("OrderData:1001");
RecordId recordId = redisTemplate.opsForStream().add(record);
// 2. 读取消息 (监听模式通常配合 StreamMessageListenerContainer 使用)
List<MapRecord<String, Object, Object>> messages = redisTemplate.opsForStream()
.read(StreamOffset.create("order_stream", ReadOffset.lastConsumed()));
}
八、 Bitmaps:位图 (极简内存占用)
场景: 1 亿用户,每天谁签到了?谁在线?
原理: 用 1 个 bit(0 或 1)表示一个状态。1 亿个 bit 仅占 12.5 MB。
实战代码:签到系统
public void bitmapOps(Long userId) {
// 设置第 userId 位为 1(签到)
redisTemplate.opsForValue().setBit("sign:202601", userId, true);
// 统计当月总签到人数
Long count = (Long)redisTemplate.execute((RedisCallback<Long>) conn ->
conn.bitCount("sign:202601".getBytes()));
// 查询用户今天是否签到
Boolean isSigned = redisTemplate.opsForValue().getBit("sign:202601", userId);
}
九、 HyperLogLog:海量基数统计
场景: 统计网页 UV(独立访客数)。
痛点: 如果用 Set 存 1000 万个 IP,非常耗内存。HyperLogLog 仅用 12 KB 就能统计 2^64 个数据,误差约 0.81%。
实战代码:UV 统计
public void hllOps(String ip) {
// 添加访问 IP
redisTemplate.opsForHyperLogLog().add("page_uv:home", ip);
// 获取预估的去重计数
Long uvCount = redisTemplate.opsForHyperLogLog().size("page_uv:home");
}
十、 Geospatial:地理位置 (附近的人)
场景: 查找附近 5 公里的外卖商家、车辆定位。
实战代码:位置计算
public void geoOps() {
// 1. 添加坐标 (经度, 纬度, 名称)
redisTemplate.opsForGeo().add("stores", new Point(116.40, 39.90), "Store_A");
redisTemplate.opsForGeo().add("stores", new Point(116.41, 39.91), "Store_B");
// 2. 计算两地距离
Distance dist = redisTemplate.opsForGeo().distance("stores", "Store_A", "Store_B", RedisGeoCommands.DistanceUnit.KILOMETERS);
// 3. 搜索半径 5km 内的商家
Circle circle = new Circle(new Point(116.40, 39.90), new Distance(5, RedisGeoCommands.DistanceUnit.KILOMETERS));
GeoResults<RedisGeoCommands.GeoLocation<Object>> results = redisTemplate.opsForGeo().radius("stores", circle);
}
十一、 Redis 过期监听 (Key Expiration Listener)
场景: 订单 30 分钟未支付自动取消(虽然通常建议用延迟队列,但小规模场景可用此功能)。
1. 开启配置 (redis.conf)
确保配置了 notify-keyspace-events Ex。
2. 编写监听器
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
if (expiredKey.startsWith("order:")) {
// 执行取消订单逻辑
System.out.println("订单已过期:" + expiredKey);
}
}
}
十二、 场景选择全汇总
| 需求场景 | 推荐特性 | 核心理由 | 业务价值 |
|---|---|---|---|
| 分布式锁 / 抢红包 / 秒杀扣减 | Lua 脚本 | 绝对原子性。多条命令在 Redis 中作为整体执行,中间不会被其他请求插队。 | 防止超卖,解决“查询-修改-写入”的并发冲突。 |
| 大批量初始化数据导入 | Pipeline (流水线) | 减少 RTT 网络往返。将数千条命令打包发送,大幅度降低网络延迟带来的开销。 | 性能提升 10 倍以上,缩短系统预热时间。 |
| 可靠的异步订单处理 / 任务队列 | Stream | 消息持久化 + 消费组 (ACK) 。支持偏移量读取和消息确认,消息执行失败可追溯。 | 相比 Pub/Sub 更加安全,实现类似 Kafka 的可靠消费。 |
| 海量用户日活 (DAU) / 签到统计 | Bitmaps (位图) | 极致内存利用率。1 个 bit 代表 1 个用户,1 亿用户状态仅需 12.5MB 内存。 | 极速完成上亿数据的位运算(并集/交集),统计活跃度。 |
| 百万级/千万级网页 UV 统计 | HyperLogLog | 概率算法统计基数。无论数据量多大,仅固定占用 12KB 内存,误差率极低。 | 在不存储具体用户 ID 的情况下,极低成本实现去重计数。 |
| 附近的人 / 附近商家查询 | Geospatial (地理位置) | 内置坐标索引。支持经纬度存储,并提供半径查询、距离计算等专用指令。 | 轻松实现 LBS(基于位置的服务),如 O2O 配送范围计算。 |
| 简单的悲观并发控制 | Transactions (事务) | WATCH + MULTI/EXEC。提供简单的乐观锁机制,监控 Key 是否被外部修改。 | 保证一组操作在执行前数据未被篡改。 |
| 即时聊天室 / 实时系统通知 | Pub/Sub (发布订阅) | 轻量级解耦。生产者只需发送,所有在线的订阅者均能实时收到,无需存储。 | 实现低延迟的实时广播,系统逻辑高度解耦。 |
| 防止数据库缓存穿透 | Bloom Filter (布隆过滤器) | 快速排除非法请求。以极小的空间开销判断“值是否一定不存在”。 | 拦截非法请求直接回源数据库,保护底层存储安全。 |
| 延迟任务执行 (如 30 分钟未支付) | Key Expiration Listener | 基于过期事件通知。当 Key 到期消失时,Redis 主动推送通知给 Java 应用。 | 实现简易的定时清理任务(注:高可靠场景仍建议用 MQ 延迟队列)。 |
⚠️ 注意事项与进阶建议
- BigKey 问题:Bitmaps 和 HLL 如果 key 过大,在分配内存或删除时会引起 Redis 阻塞。建议按时间维度(如天、月)分 key 存储。
- Stream 的堆积:Stream 消息会持久化,必须设置
MAXLEN限制长度,防止耗尽内存。 - 过期监听风险:过期监听并不保证实时性。Redis 是在 key 被真正删除(惰性删除或定期扫描)时才发通知,高负载下会有延迟。