1. 项目背景与Redis技术选型
"苍穹外卖"作为一款典型的O2O餐饮服务平台,其业务高峰期往往面临三大技术挑战:高并发订单处理、实时配送状态更新和热门商家推荐。在项目迭代到Day05阶段时,我们遇到了一个典型性能瓶颈——商家端活动页面的访问响应时间从平均200ms飙升到1.2秒,特别是在午晚高峰时段。
经过压力测试和代码分析,发现问题集中在两个环节:
- 商家促销活动数据每次请求都从MySQL查询
- 配送范围校验涉及复杂的GIS计算
Redis的引入正是为了解决这些问题。相比Memcached,Redis的优势在于:
- 支持持久化(RDB/AOF),避免缓存雪崩
- 原生Geo模块支持距离计算
- 丰富的数据结构适合不同业务场景
关键决策:选择Spring Data Redis而非Jedis直接操作,因为前者提供:
- 自动连接管理
- 异常转换
- 模板化操作
- 与Spring生态无缝集成
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis环境搭建与基础配置
2.1 Windows开发环境安装
虽然生产环境使用Linux,但开发阶段我们在Windows 10上配置Redis 5.0.14:
bash复制# 解压安装包后启动服务端
redis-server.exe redis.windows.conf
# 另开CMD窗口连接测试
redis-cli.exe -h 127.0.0.1 -p 6379
> PING
PONG
2.2 关键配置项调优
修改redis.windows.conf:
properties复制maxmemory 1GB
maxmemory-policy allkeys-lru
timeout 300
tcp-keepalive 60
2.3 Spring Boot集成配置
application.yml核心配置:
yaml复制spring:
redis:
host: localhost
port: 6379
password:
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
3. 五大数据结构的业务落地
3.1 String类型:秒杀库存控制
使用INCR/DECR实现原子性库存扣减:
java复制public boolean deductStock(String itemId, int num) {
String key = "stock:" + itemId;
Long remain = redisTemplate.opsForValue().decrement(key, num);
if(remain >= 0){
return true;
}else{
// 回滚操作
redisTemplate.opsForValue().increment(key, num);
return false;
}
}
3.2 Hash类型:商家详情缓存
java复制// 缓存商家基础信息
public void cacheMerchantInfo(Long merchantId, MerchantVO vo){
String key = "merchant:" + merchantId;
redisTemplate.opsForHash().putAll(key, BeanUtil.beanToMap(vo));
redisTemplate.expire(key, 2, TimeUnit.HOURS);
}
3.3 List类型:订单处理队列
实现异步订单处理流程:
java复制// 生产者端
redisTemplate.opsForList().rightPush("order:queue", orderId);
// 消费者端
while(true){
Object orderId = redisTemplate.opsForList().leftPop("order:queue", 10, TimeUnit.SECONDS);
if(orderId != null){
processOrder((String)orderId);
}
}
3.4 Set类型:用户标签系统
java复制// 给用户打标签
public void addUserTag(Long userId, String tag){
String key = "user:tags:" + userId;
redisTemplate.opsForSet().add(key, tag);
}
// 计算共同标签
public Set<String> getCommonTags(Long userId1, Long userId2){
String key1 = "user:tags:" + userId1;
String key2 = "user:tags:" + userId2;
return redisTemplate.opsForSet().intersect(key1, key2);
}
3.5 ZSet类型:热门商家排行
java复制// 更新商家评分
public void updateMerchantScore(Long merchantId, double score){
String key = "merchant:rank";
redisTemplate.opsForZSet().add(key, merchantId.toString(), score);
}
// 获取Top10商家
public Set<String> getTopMerchants(){
String key = "merchant:rank";
return redisTemplate.opsForZSet().reverseRange(key, 0, 9);
}
4. Geo模块实现配送范围校验
4.1 地理位置存储
java复制public void addDeliveryLocation(Long merchantId, double lng, double lat){
String key = "merchant:geo";
redisTemplate.opsForGeo().add(key, new Point(lng, lat), merchantId.toString());
}
4.2 距离计算
java复制public boolean checkDeliveryRange(Long merchantId, double userLng, double userLat, double maxDistance){
String key = "merchant:geo";
Distance distance = redisTemplate.opsForGeo()
.distance(key, merchantId.toString(),
new Point(userLng, userLat),
Metrics.KILOMETERS);
return distance != null && distance.getValue() <= maxDistance;
}
5. 缓存治理与性能优化
5.1 缓存穿透解决方案
java复制public Object getWithPenetrationProtect(String key, Class<?> type, long expire, Supplier<Object> loader){
// 布隆过滤器伪代码
if(!bloomFilter.mightContain(key)){
return null;
}
Object value = redisTemplate.opsForValue().get(key);
if(value != null){
return value;
}
synchronized (this){
value = redisTemplate.opsForValue().get(key);
if(value == null){
value = loader.get();
if(value == null){
// 空值缓存
redisTemplate.opsForValue().set(key, "", expire, TimeUnit.SECONDS);
}else{
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
}
return value;
}
}
5.2 热点Key发现与处理
使用Redis自带的监控命令:
bash复制redis-cli --hotkeys
5.3 内存优化技巧
- 使用Hash的hscan代替hgetall
- 对长文本启用压缩
- 设置合理的TTL
6. 生产环境踩坑实录
6.1 连接池配置不当
现象:高峰期出现"Could not get a resource from the pool"错误
解决方案:
yaml复制lettuce:
pool:
max-active: 50 # 根据QPS调整
max-wait: 1000ms
test-while-idle: true
time-between-eviction-runs: 30000ms
6.2 大Key导致慢查询
发现方法:
bash复制redis-cli --bigkeys
优化方案:
- 拆分Hash为多个小Key
- 使用SCAN系列命令替代KEYS
- 对ZSET进行分段存储
6.3 主从切换数据丢失
配置增强:
properties复制min-replicas-to-write 1
min-replicas-max-lag 10
7. 监控与告警体系
7.1 基础监控指标
bash复制# 内存使用
INFO memory
# 命令统计
INFO commandstats
# 客户端连接
INFO clients
7.2 Prometheus监控配置
yaml复制- job_name: 'redis'
static_configs:
- targets: ['redis-host:9121']
7.3 关键告警规则
- 内存使用率 > 80%持续5分钟
- 连接数 > max_connections*0.8
- 每秒拒绝连接数 > 10
8. Java内存调优配合Redis
8.1 典型OOM场景
java复制// 错误示例:一次性获取大结果集
Set<Object> hugeSet = redisTemplate.opsForSet().members("large:set");
// 正确做法:使用scan
Cursor<Object> cursor = redisTemplate.opsForSet().scan("large:set",
ScanOptions.scanOptions().count(100).build());
8.2 GC调优参数
bash复制-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=35
9. 开发规范与Code Review要点
9.1 键名设计规范
- 业务前缀: 如order:
- 环境标识: 如dev:order:
- 统一分隔符: 使用冒号:
9.2 禁止操作
- 生产环境禁止FLUSHALL
- 禁止使用KEYS命令
- 慎用长时间阻塞命令
9.3 事务使用原则
java复制// 错误用法:跨多个key的事务
redisTemplate.multi();
redisTemplate.opsForValue().set("key1", "v1");
redisTemplate.opsForSet().add("key2", "v2");
redisTemplate.exec();
// 正确做法:使用Lua脚本
String script = "redis.call('set', KEYS[1], ARGV[1]);" +
"redis.call('sadd', KEYS[2], ARGV[2]);";
redisTemplate.execute(
new DefaultRedisScript<>(script),
Arrays.asList("key1", "key2"),
"v1", "v2"
);
10. 扩展应用场景
10.1 分布式锁进阶实现
java复制public boolean tryLock(String lockKey, String requestId, long expireTime){
return redisTemplate.opsForValue().setIfAbsent(
lockKey,
requestId,
expireTime,
TimeUnit.MILLISECONDS
);
}
public boolean unlock(String lockKey, String requestId){
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
return redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
Collections.singletonList(lockKey),
requestId
);
}
10.2 延迟队列实现
java复制// 添加延迟任务
public void addDelayTask(String taskId, long delayTime){
String queueKey = "delay:queue";
String taskKey = "delay:task:" + taskId;
redisTemplate.opsForValue().set(taskKey, "pending");
redisTemplate.opsForZSet().add(queueKey, taskId,
System.currentTimeMillis() + delayTime);
}
// 处理到期任务
public void processDelayTasks(){
String queueKey = "delay:queue";
Set<String> taskIds = redisTemplate.opsForZSet().rangeByScore(
queueKey,
0,
System.currentTimeMillis(),
0,
10
);
for(String taskId : taskIds){
if(redisTemplate.opsForZSet().remove(queueKey, taskId) > 0){
String taskKey = "delay:task:" + taskId;
String status = (String)redisTemplate.opsForValue().get(taskKey);
if("pending".equals(status)){
executeTask(taskId);
}
}
}
}
