前言
一次线上事故让我印象深刻:数据库CPU突然飙到100%,服务全面超时,排查后发现是缓存没有正确使用,大量请求直接打到了MySQL。
引入Redis并做好缓存设计之后,接口响应时间从800ms降到了20ms以内。
这篇文章分享Redis在真实项目中的实战用法。
一、Redis核心数据结构
String(字符串):
最基础的类型,值可以是字符串、数字、二进制
适用:缓存、计数器、分布式锁
Hash(哈希):
类似Map<String, String>,存储对象字段
适用:用户信息、商品详情等结构化对象
List(列表):
双向链表,支持头尾插入/弹出
适用:消息队列、最新动态列表
Set(集合):
无序不重复集合,支持交并差集操作
适用:共同好友、标签系统
Sorted Set(有序集合):
每个元素附带score,按分值排序
适用:排行榜、延迟队列
特殊结构:
HyperLogLog → UV统计(误差0.81%,内存极省)
Bitmap → 用户签到、在线状态
Stream → 持久化消息队列
二、Spring Boot集成Redis
2.1 基础配置
yaml
# application.yml
spring:
redis:
host: localhost
port: 6379
password: ${REDIS_PASSWORD}
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 20 # 最大连接数
max-idle: 10 # 最大空闲连接
min-idle: 5 # 最小空闲连接
max-wait: 1000ms # 获取连接最大等待时间
java
// RedisConfig.java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key使用String序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value使用JSON序列化
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
return template;
}
}
2.2 封装通用缓存工具类
java
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 设置缓存(带过期时间)
public void set(String key, Object value, long seconds) {
redisTemplate.opsForValue().set(key, value, seconds, TimeUnit.SECONDS);
}
// 获取缓存
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
// 删除缓存
public void delete(String key) {
redisTemplate.delete(key);
}
// 原子自增(计数器)
public Long increment(String key) {
return redisTemplate.opsForValue().increment(key);
}
// 判断key是否存在
public boolean exists(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
// 设置过期时间
public void expire(String key, long seconds) {
redisTemplate.expire(key, seconds, TimeUnit.SECONDS);
}
}
三、高频业务场景实战
3.1 缓存穿透、击穿、雪崩
缓存穿透:
查询一个根本不存在的数据
请求直接穿透缓存,全部打到DB
解决:布隆过滤器 + 空值缓存
缓存击穿:
热点key突然过期
大量请求同时涌入DB
解决:互斥锁 + 逻辑过期
缓存雪崩:
大量key同时过期
DB瞬间承受大量请求
解决:过期时间加随机值 + 熔断降级
java
// 缓存击穿解决方案:分布式互斥锁
public UserDTO getUserById(Long userId) {
String cacheKey = "user:" + userId;
// 1. 先查缓存
UserDTO user = (UserDTO) redisUtil.get(cacheKey);
if (user != null) return user;
// 2. 缓存未命中,加分布式锁
String lockKey = "lock:user:" + userId;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 3. 双重检查(防止并发情况下重复查DB)
user = (UserDTO) redisUtil.get(cacheKey);
if (user != null) return user;
// 4. 查数据库
user = userMapper.selectById(userId);
// 5. 空值也缓存(防止缓存穿透)
if (user == null) {
redisUtil.set(cacheKey, "", 60);
return null;
}
// 6. 写入缓存(过期时间加随机值,防雪崩)
long ttl = 3600 + new Random().nextInt(600);
redisUtil.set(cacheKey, user, ttl);
} finally {
// 7. 释放锁
redisUtil.delete(lockKey);
}
} else {
// 未拿到锁,短暂等待后重试
Thread.sleep(50);
return getUserById(userId);
}
return user;
}
3.2 排行榜(Sorted Set)
java
@Service
public class LeaderboardService {
private static final String RANK_KEY = "game:score:rank";
// 更新分数
public void updateScore(String userId, double score) {
redisTemplate.opsForZSet().add(RANK_KEY, userId, score);
}
// 增加分数
public void addScore(String userId, double delta) {
redisTemplate.opsForZSet().incrementScore(RANK_KEY, userId, delta);
}
// 获取Top N(从高到低)
public List<RankItem> getTopN(int n) {
Set<ZSetOperations.TypedTuple<Object>> result =
redisTemplate.opsForZSet()
.reverseRangeWithScores(RANK_KEY, 0, n - 1);
List<RankItem> rankList = new ArrayList<>();
int rank = 1;
for (ZSetOperations.TypedTuple<Object> item : result) {
rankList.add(new RankItem(
rank++,
(String) item.getValue(),
item.getScore()
));
}
return rankList;
}
// 获取用户排名
public Long getUserRank(String userId) {
Long rank = redisTemplate.opsForZSet()
.reverseRank(RANK_KEY, userId);
return rank != null ? rank + 1 : null;
}
}
3.3 分布式限流(滑动窗口)
java
@Component
public class RateLimiter {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 滑动窗口限流
* @param key 限流Key(如 "rate:userId:api")
* @param limit 窗口内最大请求数
* @param windowMs 窗口大小(毫秒)
*/
public boolean isAllowed(String key, int limit, long windowMs) {
long now = System.currentTimeMillis();
long windowStart = now - windowMs;
// Lua脚本保证原子性
String script =
"redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])\n" +
"local count = redis.call('ZCARD', KEYS[1])\n" +
"if count < tonumber(ARGV[2]) then\n" +
" redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])\n" +
" redis.call('PEXPIRE', KEYS[1], ARGV[4])\n" +
" return 1\n" +
"end\n" +
"return 0";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
String.valueOf(windowStart),
String.valueOf(limit),
String.valueOf(now),
String.valueOf(windowMs)
);
return Long.valueOf(1).equals(result);
}
}
// 使用示例(Controller层)
@GetMapping("/api/data")
public Result getData(HttpServletRequest request) {
String key = "rate:" + request.getRemoteAddr() + ":data";
// 每分钟最多请求60次
if (!rateLimiter.isAllowed(key, 60, 60000)) {
return Result.fail("请求过于频繁,请稍后再试");
}
return Result.ok(dataService.getData());
}
3.4 用户签到(Bitmap)
java
@Service
public class SignInService {
// 签到
public void signIn(Long userId) {
String key = "sign:" + userId + ":" +
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
int dayOfMonth = LocalDate.now().getDayOfMonth();
// 偏移量从0开始,第1天对应offset=0
redisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
// 设置过期时间(多保留一个月)
redisTemplate.expire(key, 60, TimeUnit.DAYS);
}
// 查询本月签到天数
public Long countSignIn(Long userId) {
String key = "sign:" + userId + ":" +
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
return redisTemplate.execute(
(RedisCallback<Long>) conn ->
conn.bitCount(key.getBytes())
);
}
// 查询某天是否签到
public Boolean checkSignIn(Long userId, int day) {
String key = "sign:" + userId + ":" +
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
return redisTemplate.opsForValue().getBit(key, day - 1);
}
}
四、分布式锁标准实现
java
@Component
public class DistributedLock {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String LOCK_PREFIX = "dlock:";
/**
* 加锁
* @param lockName 锁名称
* @param requestId 唯一请求ID(用于安全解锁)
* @param expireMs 超时时间(毫秒)
*/
public boolean tryLock(String lockName, String requestId, long expireMs) {
String key = LOCK_PREFIX + lockName;
return Boolean.TRUE.equals(
redisTemplate.opsForValue()
.setIfAbsent(key, requestId, expireMs, TimeUnit.MILLISECONDS)
);
}
/**
* 安全解锁(Lua脚本保证原子性)
* 只有持有锁的请求才能解锁,防止误删他人的锁
*/
public boolean unlock(String lockName, String requestId) {
String key = LOCK_PREFIX + lockName;
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then\n" +
" return redis.call('del', KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
requestId
);
return Long.valueOf(1).equals(result);
}
}
// 使用示例(扣减库存)
public boolean deductStock(Long productId, int quantity) {
String lockName = "stock:" + productId;
String requestId = UUID.randomUUID().toString();
if (!distributedLock.tryLock(lockName, requestId, 5000)) {
throw new RuntimeException("系统繁忙,请重试");
}
try {
// 查询库存
int stock = productMapper.getStock(productId);
if (stock < quantity) {
return false;
}
// 扣减库存
productMapper.deductStock(productId, quantity);
return true;
} finally {
distributedLock.unlock(lockName, requestId);
}
}
五、Redis集群方案对比
主从复制(Master-Slave):
一主多从,读写分离
适合:读多写少,数据量不大
缺点:主节点故障需手动切换
哨兵模式(Sentinel):
在主从基础上增加哨兵节点
自动故障转移
适合:高可用要求高,数据量适中
Cluster集群:
数据分片,横向扩展
16384个哈希槽分布到多个节点
适合:数据量大,高并发写入
缺点:多key操作受限
bash
# 查看集群节点状态
redis-cli -c -h 127.0.0.1 -p 7001 cluster nodes
# 集群信息
redis-cli -c cluster info
# 查看key落在哪个slot
redis-cli cluster keyslot "user:10001"
六、运维监控
bash
# 查看内存使用
redis-cli info memory | grep used_memory_human
# 查看慢查询日志
redis-cli slowlog get 10
# 查找大key(危险操作,生产慎用)
redis-cli --bigkeys
# 实时监控命令(生产慎用)
redis-cli monitor
# 查看连接数
redis-cli info clients | grep connected_clients
# 持久化状态
redis-cli info persistence
七、团队与工具
我们在做Redis集群选型方案时,与海外架构团队进行了多次视频评审会议,大量涉及技术细节的讨论对翻译准确性要求极高。全程使用**同言翻译(Transync AI)**的实时语音翻译功能,专业术语翻译准确,帮我们顺利完成了集群方案的最终确认。
八、最佳实践检查清单
□ Key命名规范:业务:模块:id(如 user:profile:10001)
□ 必须设置过期时间,避免内存无限增长
□ 避免存储超过10KB的大Value
□ 禁止在生产环境使用KEYS命令(改用SCAN)
□ 缓存与DB更新保持一致性策略(先删后写 / 延迟双删)
□ 热点数据做本地缓存二级缓存
□ 连接池参数根据并发量合理配置
□ 开启持久化(AOF + RDB双保险)
□ 集群环境至少3主3从
□ 定期检查慢查询日志
总结
Redis在项目中的核心价值:
- ⚡ 极致性能:读写10万+ QPS,延迟微秒级
- 🔒 分布式协调:分布式锁、限流、session共享
- 📊 丰富数据结构:每种结构对应一类业务场景
- 🛡️ 保护数据库:缓存层抵挡大量无效请求
掌握Redis不只是会几个set/get命令,更重要的是理解每种场景下的设计思路,在缓存一致性、内存管理、高可用之间做出合理的权衡取舍。

