Skip to content

第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 优化建议

  1. 使用Lua脚本:将库存检查和扣减逻辑放在Lua脚本中执行,减少网络往返时间,进一步提高并发性能

  2. 增加库存预占:对于秒杀等场景,可以先预占库存,然后在一定时间内完成支付,超时释放库存

  3. 使用Redis Cluster:在高并发场景下,使用Redis集群分散负载

  4. 监控与告警:实时监控库存变化和锁的使用情况,设置合理的告警机制

  5. 降级策略:当Redis不可用时,能够优雅降级到数据库操作

实战总结

通过以上两个进阶实战案例,我们学习了:

  1. 简单缓存系统

    • 完整的缓存策略设计
    • 缓存穿透、击穿、雪崩的防护措施
    • 不同数据类型的合理使用
    • 与后端语言的集成实现
  2. 分布式锁实现库存扣减

    • 分布式锁的原理和实现
    • 高并发场景下的库存管理
    • 事务的使用和原子性保证
    • 死锁的避免和处理

这些实战案例覆盖了Redis在企业级应用中的核心场景,是从新手到进阶的重要练习。在实际项目中,我们可以根据具体需求进行调整和优化,以达到最佳的性能和可靠性。

© 2026 编程马·菜鸟教程 版权所有