Appearance
第16章:进阶实战(贴合企业开发,完整项目)
实战4:简单缓存系统(综合命令实战)
16.1 需求分析
设计一个简单但功能完整的缓存系统,需要支持:
- 数据缓存:将数据库查询结果缓存到Redis
- 过期清理:自动清理过期缓存
- 缓存更新:当数据变化时更新缓存
- 缓存穿透防护:防止查询不存在的数据导致缓存失效
- 缓存击穿防护:防止热点数据过期导致大量请求打数据库
- 缓存雪崩防护:防止大量缓存同时过期
16.2 核心实现
- 多数据类型命令:String(存储简单数据)、Hash(存储复杂对象)
- 过期时间设置:使用EX参数设置合理的过期时间
- 缓存策略:先查缓存,缓存未命中再查数据库并更新缓存
- 缓存穿透防护:缓存空值,设置较短的过期时间
- 缓存击穿防护:使用分布式锁,确保只有一个线程去数据库查询
- 缓存雪崩防护:过期时间加随机值,避免同时过期
16.3 系统设计
1. 缓存键命名规范
cache:{业务模块}:{数据类型}:{唯一标识}示例:
cache:user:info:123- 用户ID为123的用户信息cache:product:detail:1001- 商品ID为1001的商品详情
2. 缓存系统架构
客户端 → 缓存系统 → Redis → 数据库16.4 代码实现(Java示例)
1. 缓存工具类
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.concurrent.TimeUnit;
public class CacheUtils {
private static final JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(20);
config.setMaxWaitMillis(10000);
jedisPool = new JedisPool(config, "localhost", 6379);
}
// 获取缓存
public static String get(String key) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.get(key);
}
}
// 设置缓存
public static void set(String key, String value, int expireSeconds) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(key, expireSeconds, value);
}
}
// 删除缓存
public static void delete(String key) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.del(key);
}
}
// 检查缓存是否存在
public static boolean exists(String key) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.exists(key);
}
}
// 设置带随机过期时间的缓存(防止缓存雪崩)
public static void setWithRandomExpire(String key, String value, int baseExpireSeconds, int randomRange) {
int expireTime = baseExpireSeconds + (int)(Math.random() * randomRange);
set(key, value, expireTime);
}
}2. 缓存服务实现
java
import com.alibaba.fastjson.JSON;
public class CacheService {
// 获取用户信息(带缓存)
public User getUserInfo(int userId) {
String key = "cache:user:info:" + userId;
// 先查缓存
String cachedData = CacheUtils.get(key);
if (cachedData != null) {
// 缓存命中
if ("NULL".equals(cachedData)) {
// 缓存穿透防护:返回空对象
return null;
}
return JSON.parseObject(cachedData, User.class);
}
// 缓存未命中,查数据库
User user = userDao.getUserById(userId);
if (user != null) {
// 缓存存在的用户信息,设置1小时过期,加随机值防止雪崩
CacheUtils.setWithRandomExpire(key, JSON.toJSONString(user), 3600, 300);
} else {
// 缓存穿透防护:缓存空值,设置5分钟过期
CacheUtils.set(key, "NULL", 300);
}
return user;
}
// 更新用户信息
public void updateUserInfo(User user) {
// 更新数据库
userDao.updateUser(user);
// 更新缓存
String key = "cache:user:info:" + user.getId();
CacheUtils.setWithRandomExpire(key, JSON.toJSONString(user), 3600, 300);
}
// 删除用户信息
public void deleteUserInfo(int userId) {
// 删除数据库记录
userDao.deleteUser(userId);
// 删除缓存
String key = "cache:user:info:" + userId;
CacheUtils.delete(key);
}
}16.5 缓存击穿防护实现
java
import redis.clients.jedis.Jedis;
public class CacheBreakdownProtection {
// 获取热点数据(带缓存击穿防护)
public Product getHotProduct(int productId) {
String key = "cache:product:detail:" + productId;
String lockKey = "lock:product:detail:" + productId;
// 先查缓存
String cachedData = CacheUtils.get(key);
if (cachedData != null) {
return JSON.parseObject(cachedData, Product.class);
}
// 缓存未命中,尝试获取分布式锁
try (Jedis jedis = jedisPool.getResource()) {
// 设置分布式锁,过期时间10秒
String result = jedis.set(lockKey, "1", "NX", "EX", 10);
if ("OK".equals(result)) {
// 获取锁成功,查询数据库
Product product = productDao.getProductById(productId);
if (product != null) {
// 缓存热点数据,设置较长过期时间
CacheUtils.setWithRandomExpire(key, JSON.toJSONString(product), 7200, 600);
} else {
// 缓存穿透防护
CacheUtils.set(key, "NULL", 300);
}
// 释放锁
jedis.del(lockKey);
return product;
} else {
// 获取锁失败,等待一段时间后重试
Thread.sleep(100);
return getHotProduct(productId);
}
} catch (Exception e) {
e.printStackTrace();
// 异常情况下直接查数据库
return productDao.getProductById(productId);
}
}
}实战5:分布式锁实现库存扣减(高级特性实战)
16.4 需求分析
在高并发场景下实现商品库存扣减系统,需要:
- 防止超卖:确保库存不会被减到负数
- 保证原子性:扣减库存和增加销量的操作必须原子执行
- 支持高并发:在大量请求同时到来时保持系统稳定
- 避免死锁:确保锁能够正确释放
16.5 核心实现
- 分布式锁:使用SET NX EX命令实现,确保同一时间只有一个线程操作库存
- 事务:使用Redis事务保证多个命令的原子执行
- 库存检查:在扣减前检查库存是否充足
- 过期时间:为锁设置合理的过期时间,避免死锁
- 持久化配置:开启AOF持久化,确保数据安全
16.6 代码实现(Node.js示例)
1. 分布式锁工具类
javascript
const redis = require('redis');
const client = redis.createClient();
class DistributedLock {
// 获取分布式锁
static async acquireLock(lockKey, expireTime = 10) {
return new Promise((resolve) => {
client.set(lockKey, '1', 'NX', 'EX', expireTime, (err, result) => {
resolve(result === 'OK');
});
});
}
// 释放分布式锁
static async releaseLock(lockKey) {
return new Promise((resolve) => {
client.del(lockKey, (err, result) => {
resolve(result > 0);
});
});
}
}
module.exports = DistributedLock;2. 库存扣减服务
javascript
const redis = require('redis');
const client = redis.createClient();
const DistributedLock = require('./DistributedLock');
class InventoryService {
// 扣减库存
static async deductInventory(productId, quantity = 1) {
const stockKey = `product:${productId}:stock`;
const salesKey = `product:${productId}:sales`;
const lockKey = `lock:product:${productId}:inventory`;
try {
// 获取分布式锁
const acquired = await DistributedLock.acquireLock(lockKey, 5);
if (!acquired) {
// 未获取到锁,稍后重试
await new Promise(resolve => setTimeout(resolve, 100));
return await this.deductInventory(productId, quantity);
}
// 获取当前库存
const currentStock = await new Promise((resolve) => {
client.get(stockKey, (err, result) => {
resolve(parseInt(result) || 0);
});
});
// 检查库存是否充足
if (currentStock < quantity) {
await DistributedLock.releaseLock(lockKey);
return { success: false, message: '库存不足' };
}
// 开启事务
client.multi();
// 扣减库存
client.decrby(stockKey, quantity);
// 增加销量
client.incrby(salesKey, quantity);
// 执行事务
const result = await new Promise((resolve) => {
client.exec((err, replies) => {
resolve(replies);
});
});
// 释放锁
await DistributedLock.releaseLock(lockKey);
if (result && result.length === 2) {
return {
success: true,
message: '库存扣减成功',
newStock: result[0],
newSales: result[1]
};
} else {
return { success: false, message: '库存扣减失败' };
}
} catch (error) {
console.error('库存扣减错误:', error);
// 确保锁被释放
await DistributedLock.releaseLock(lockKey).catch(() => {});
return { success: false, message: '系统错误' };
}
}
// 初始化商品库存
static async initProductInventory(productId, initialStock) {
const stockKey = `product:${productId}:stock`;
const salesKey = `product:${productId}:sales`;
client.set(stockKey, initialStock);
client.set(salesKey, 0);
return { success: true, message: '初始化库存成功' };
}
// 获取商品库存和销量
static async getProductInventory(productId) {
const stockKey = `product:${productId}:stock`;
const salesKey = `product:${productId}:sales`;
const stock = await new Promise((resolve) => {
client.get(stockKey, (err, result) => {
resolve(parseInt(result) || 0);
});
});
const sales = await new Promise((resolve) => {
client.get(salesKey, (err, result) => {
resolve(parseInt(result) || 0);
});
});
return { stock, sales };
}
}
module.exports = InventoryService;16.7 高并发测试
1. 测试脚本
javascript
const InventoryService = require('./InventoryService');
async function testHighConcurrency() {
const productId = 1001;
const initialStock = 100;
// 初始化库存
await InventoryService.initProductInventory(productId, initialStock);
console.log(`初始化商品${productId}库存为${initialStock}`);
// 模拟100个并发请求,每个请求扣减1个库存
const requests = [];
for (let i = 0; i < 150; i++) {
requests.push(InventoryService.deductInventory(productId, 1));
}
// 执行所有请求
const results = await Promise.all(requests);
// 统计结果
let successCount = 0;
let failureCount = 0;
results.forEach(result => {
if (result.success) {
successCount++;
} else {
failureCount++;
}
});
// 获取最终库存和销量
const finalInventory = await InventoryService.getProductInventory(productId);
console.log(`\n测试结果:`);
console.log(`总请求数:${requests.length}`);
console.log(`成功数:${successCount}`);
console.log(`失败数:${failureCount}`);
console.log(`最终库存:${finalInventory.stock}`);
console.log(`最终销量:${finalInventory.sales}`);
console.log(`库存+销量:${finalInventory.stock + finalInventory.sales}`);
console.log(`是否超卖:${finalInventory.stock < 0}`);
}
testHighConcurrency().catch(console.error);2. 测试结果分析
执行测试脚本后,我们应该看到:
- 成功扣减的数量等于初始库存(100)
- 失败的数量等于超出库存的请求数(50)
- 最终库存为0,销量为100
- 库存+销量等于初始库存(100)
- 没有出现超卖(库存为负数)的情况
16.8 优化建议
使用Lua脚本:将库存检查和扣减逻辑放在Lua脚本中执行,减少网络往返时间,进一步提高并发性能
增加库存预占:对于秒杀等场景,可以先预占库存,然后在一定时间内完成支付,超时释放库存
使用Redis Cluster:在高并发场景下,使用Redis集群分散负载
监控与告警:实时监控库存变化和锁的使用情况,设置合理的告警机制
降级策略:当Redis不可用时,能够优雅降级到数据库操作
实战总结
通过以上两个进阶实战案例,我们学习了:
简单缓存系统:
- 完整的缓存策略设计
- 缓存穿透、击穿、雪崩的防护措施
- 不同数据类型的合理使用
- 与后端语言的集成实现
分布式锁实现库存扣减:
- 分布式锁的原理和实现
- 高并发场景下的库存管理
- 事务的使用和原子性保证
- 死锁的避免和处理
这些实战案例覆盖了Redis在企业级应用中的核心场景,是从新手到进阶的重要练习。在实际项目中,我们可以根据具体需求进行调整和优化,以达到最佳的性能和可靠性。
