第 5 课:Redis 常见应用场景(上)
Redis 在企业项目中最常见的用途就是:
- 缓存
- 分布式锁
- 解决高并发问题
本课将通过实际项目案例学习 Redis 的核心应用场景。
5.1 缓存
缓存是 Redis 最常用的功能,可以显著提升系统性能,减少数据库压力。
缓存流程
客户端请求
↓
查询 Redis
↓
存在?
┌──Yes──→ 返回数据
│
No
│
↓
查询 MySQL
↓
写入 Redis
↓
返回结果
代码示例
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@Resource
private RedisUtil redisUtil;
public User getUserById(Long userId) {
String key = "user:" + userId;
// 1. 查询Redis
User user = (User) redisUtil.get(key);
if (user != null) {
return user;
}
// 2. Redis没有数据
user = userMapper.selectById(userId);
if (user != null) {
// 3. 写入Redis
redisUtil.set(
key,
user,
1,
TimeUnit.HOURS
);
}
return user;
}
/**
* 更新用户
*/
public void updateUser(User user) {
userMapper.updateById(user);
// 删除缓存
redisUtil.delete("user:" + user.getId());
}
/**
* 删除用户
*/
public void deleteUser(Long userId) {
userMapper.deleteById(userId);
redisUtil.delete("user:" + userId);
}
}
缓存常见问题
1、缓存穿透
问题
查询一个根本不存在的数据。
例如:
user:999999999
数据库中不存在。
此时:
Redis没有
↓
MySQL没有
↓
Redis没有缓存
↓
再次访问数据库
↓
无限循环
最终导致数据库压力暴增。
解决方案一:缓存空对象
if(user == null){
redisUtil.set(
key,
new NullUser(),
5,
TimeUnit.MINUTES
);
return null;
}
优点:
- 实现简单
- 效果明显
缺点:
- 占用少量内存
解决方案二:布隆过滤器
流程:
请求
↓
BloomFilter
↓
存在?
┌──No──→ 直接返回
│
Yes
│
↓
Redis
↓
MySQL
优点:
- 性能高
- 节省数据库资源
缺点:
- 存在误判率
2、缓存击穿
问题
某个热点 Key 过期。
例如:
user:1
恰好:
10万用户同时访问
结果:
Redis失效
↓
10万请求同时打到数据库
↓
数据库崩溃
解决方案
方案一:热点数据永不过期
redisUtil.set(key,user);
缺点:
需要手动维护缓存。
方案二:互斥锁
只有一个线程查询数据库。
其他线程等待。
代码实现
public User getUserById(Long userId) {
String key = "user:" + userId;
User user = (User) redisUtil.get(key);
if (user != null) {
return user;
}
String lockKey = "lock:user:" + userId;
String lockValue =
UUID.randomUUID().toString();
try {
// 获取锁
if (
redisUtil.setIfAbsent(
lockKey,
lockValue,
30,
TimeUnit.SECONDS
)
) {
user =
userMapper.selectById(userId);
if (user != null) {
redisUtil.set(
key,
user,
1,
TimeUnit.HOURS
);
} else {
redisUtil.set(
key,
new NullUser(),
5,
TimeUnit.MINUTES
);
}
} else {
Thread.sleep(100);
return getUserById(userId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (
lockValue.equals(
redisUtil.get(lockKey)
)
) {
redisUtil.delete(lockKey);
}
}
return user;
}
3、缓存雪崩
问题
大量缓存同时过期。
例如:
100万个Key
全部设置1小时过期
到达:
1小时整
结果:
Redis大量失效
↓
请求全部打到数据库
↓
数据库压力暴增
解决方案
方案一:随机过期时间
错误做法:
redisUtil.set(
key,
value,
60,
TimeUnit.MINUTES
);
正确做法:
int timeout =
60 + RandomUtil.randomInt(30);
redisUtil.set(
key,
value,
timeout,
TimeUnit.MINUTES
);
这样缓存不会同时失效。
方案二:Redis 集群
热点数据
↓
分散到多个节点
降低单节点压力。
方案三:熔断降级
当数据库压力过大时:
关闭非核心功能
限制访问
快速失败
保证核心业务可用。
5.2 分布式锁
为什么需要分布式锁
单机项目:
synchronized
即可解决并发问题。
但是:
服务器A
服务器B
服务器C
各自拥有自己的 JVM。
此时:
synchronized
已经失效。
需要:
Redis分布式锁
Redis 分布式锁原理
核心命令:
SET key value NX EX timeout
参数说明:
| 参数 | 说明 |
|---|---|
| NX | Key 不存在才创建 |
| EX | 设置过期时间 |
| key | 锁名称 |
| value | UUID |
| timeout | 超时时间 |
分布式锁实现
@Service
public class DistributedLockService {
@Resource
private RedisUtil redisUtil;
/**
* 获取锁
*/
public boolean tryLock(
String key,
String value,
long timeout
) {
return redisUtil.setIfAbsent(
key,
value,
timeout,
TimeUnit.SECONDS
);
}
/**
* 释放锁
*/
public boolean releaseLock(
String key,
String value
) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
Long result =
(Long) redisUtil.execute(
script,
Collections.singletonList(key),
value
);
return Long.valueOf(1)
.equals(result);
}
}
秒杀案例(防止超卖)
业务流程
用户秒杀
↓
获取分布式锁
↓
检查库存
↓
扣减库存
↓
创建订单
↓
释放锁
实现代码
@Service
public class SeckillService {
@Resource
private ProductMapper productMapper;
@Resource
private OrderMapper orderMapper;
@Resource
private DistributedLockService lockService;
public String seckill(
Long productId,
Long userId
) {
String lockKey =
"lock:product:" + productId;
String lockValue =
UUID.randomUUID().toString();
try {
if (
!lockService.tryLock(
lockKey,
lockValue,
30
)
) {
return "系统繁忙,请稍后再试";
}
// 查询库存
Product product =
productMapper.selectById(productId);
if (product.getStock() <= 0) {
return "商品已售罄";
}
// 扣减库存
product.setStock(
product.getStock() - 1
);
productMapper.updateById(product);
// 创建订单
Order order = new Order();
order.setProductId(productId);
order.setUserId(userId);
orderMapper.insert(order);
return "秒杀成功";
} finally {
lockService.releaseLock(
lockKey,
lockValue
);
}
}
}
本课知识总结
| 模块 | 核心内容 |
|---|---|
| Cache | Redis缓存 |
| Cache Penetration | 缓存穿透 |
| Cache Breakdown | 缓存击穿 |
| Cache Avalanche | 缓存雪崩 |
| Distributed Lock | 分布式锁 |
| Seckill | 秒杀系统 |
课后练习
练习 1
实现完整用户缓存功能:
- 查询缓存
- 查询数据库
- 回写缓存
- 删除缓存
练习 2
实现缓存穿透解决方案:
- 缓存空对象
- 布隆过滤器
练习 3
实现缓存击穿解决方案:
- 热点数据永不过期
- Redis 互斥锁
练习 4
实现缓存雪崩解决方案:
- 随机过期时间
- Redis 集群
- 熔断降级
练习 5
使用 Redis 分布式锁实现秒杀系统:
要求:
- 防止超卖
- UUID 唯一标识
- Lua 原子释放锁
- 支持高并发测试