问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

高并发秒杀系统开发全流程指南(第六期):缓存设计与数据一致性保障

创作时间:
作者:
@小白创作中心

高并发秒杀系统开发全流程指南(第六期):缓存设计与数据一致性保障

引用
CSDN
1.
https://blog.csdn.net/2301_78858041/article/details/145675445

在高并发秒杀系统中,缓存设计是提升系统性能和用户体验的关键环节。通过合理设计缓存机制,我们可以显著减少数据库压力、加快响应速度,并提升系统的可用性。本文将聚焦于缓存设计部分,从技术选型到核心策略,再到数据一致性保障,为你详细讲解如何打造一个高效、可靠的缓存层。

缓存的作用与技术选型

缓存的核心作用

  • 加速数据访问:将频繁访问的数据存储在内存中,减少数据库访问延迟。
  • 缓解数据库压力:通过分担数据库的读写压力,提升系统整体性能。
  • 提高系统可用性:在数据库不可用时,缓存可以作为应急数据源。

为什么选择 Redis?

  • 高性能:Redis 是基于内存的存储系统,适用于高并发场景。
  • 丰富的数据结构:支持字符串、列表、哈希、集合、有序集合等多种数据结构。
  • 持久化:支持 RDB 和 AOF 持久化方式,保障数据不丢失。
  • 高可用性:支持主从复制和哨兵模式,确保高可用性。

Redis 配置与优化

基础配置

以下是 Redis 的基础配置示例:

# Redis 配置文件示例 
port 6379 
bind 0.0.0.0 
daemonize yes 
pidfile /var/run/redis.pid  
loglevel notice 
logfile /var/log/redis.log  
dbfilename dump.rdb  
dir /var/lib/redis 
save 900 1 
save 300 10 
save 60 10000 
appendonly yes   

持久化配置

Redis 提供两种持久化方式:

  • RDB 持久化:适合备份和恢复,但可能会丢失部分数据。
  
save 900 1 
save 300 10 
save 60 10000   
  • AOF 持久化:提供更高的数据安全性,但文件较大。
  
appendonly yes   

内存优化

为了提升 Redis 的性能,可以配置最大内存并启用 LRU 等淘汰策略:

  
maxmemory 4gb 
maxmemory-policy allkeys-lru   

网络配置

为了提升 Redis 的网络性能,可以调整 TCP 参数:

  
tcp-backlog 511 
timeout 0 
keepalive 60   

缓存设计的核心策略

热点数据缓存

热点数据是指被高频访问的数据,如热门商品信息。通过将热点数据缓存到 Redis 中,可以显著提升访问速度。

@Service 
public class ProductService {
 
    @Autowired 
    private RedisTemplate<String, Object> redisTemplate;
 
    @Autowired 
    private ProductRepository productRepository;
 
    public Product getProductById(String productId) {
        // 先从 Redis 中获取 
        Product product = (Product) redisTemplate.opsForValue().get("product:"  + productId);
        if (product != null) {
            return product;
        }
        // 如果 Redis 中没有,从数据库中获取 
        product = productRepository.findById(productId).orElse(null); 
        if (product != null) {
            // 将数据存入 Redis 并设置过期时间 
            redisTemplate.opsForValue().set("product:"  + productId, product, 3600, TimeUnit.SECONDS);
        }
        return product;
    }
}  

缓存预热

缓存预热是指在系统启动时预先加载热点数据到 Redis 中,避免初次访问时的延迟。

@Component 
public class CachePreheater implements CommandLineRunner {
 
    @Autowired 
    private ProductService productService;
 
    @Override 
    public void run(String... args) throws Exception {
        List<Product> hotProducts = productService.getHotProducts(); 
        for (Product product : hotProducts) {
            productService.getProductById(product.getId().toString()); 
        }
    }
}  

分布式锁

在高并发场景下,分布式锁可以有效避免并发冲突。Redis 提供了 RedLock 算法来实现高可用的分布式锁。

@Service 
public class StockService {
 
    @Autowired 
    private RedissonClient redissonClient;
 
    @Autowired 
    private ProductRepository productRepository;
 
    public boolean decreaseStock(String productId, int num) {
        RLock lock = redissonClient.getLock("stock_lock:"  + productId);
        try {
            boolean isLocked = lock.tryLock(10,  10, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new RuntimeException("获取锁失败");
            }
            Product product = productRepository.findById(productId).orElseThrow(()  -> new RuntimeException("商品不存在"));
            if (product.getStock()  < num) {
                throw new RuntimeException("库存不足");
            }
            product.setStock(product.getStock()  - num);
            productRepository.save(product); 
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); 
            throw new RuntimeException("线程中断", e);
        } finally {
            if (lock.isHeldByCurrentThread())  {
                lock.unlock(); 
            }
        }
    }
}  

缓存穿透、击穿与雪崩

  • 缓存穿透:查询不存在的数据导致数据库压力过大。
  • 解决方案:布隆过滤器或缓存空值。
  • 缓存击穿:热点数据过期导致大量请求同时访问数据库。
  • 解决方案:互斥锁或永不过期。
  • 缓存雪崩:大量缓存同时过期导致系统崩溃。
  • 解决方案:随机过期时间或续命机制。

布隆过滤器实现

@Component 
public class BloomFilterUtil {
 
    private static final int DEFAULT_SIZE = 1 << 24;
    private static final int DEFAULT_HASH_COUNT = 5;
 
    private BitSet bitSet = new BitSet(DEFAULT_SIZE);
    private Random random = new Random();
 
    public void add(String key) {
        for (int i = 0; i < DEFAULT_HASH_COUNT; i++) {
            int index = Math.abs(random.nextInt())  % DEFAULT_SIZE;
            bitSet.set(index,  true);
        }
    }
 
    public boolean contains(String key) {
        for (int i = 0; i < DEFAULT_HASH_COUNT; i++) {
            int index = Math.abs(random.nextInt())  % DEFAULT_SIZE;
            if (!bitSet.get(index))  {
                return false;
            }
        }
        return true;
    }
}  

永不过期缓存

@Service 
public class ProductService {
 
    @Autowired 
    private RedisTemplate<String, Object> redisTemplate;
 
    @Autowired 
    private ProductRepository productRepository;
 
    public Product getProductById(String productId) {
        Product product = (Product) redisTemplate.opsForValue().get("product:"  + productId);
        if (product != null) {
            return product;
        }
        synchronized (this) {
            product = (Product) redisTemplate.opsForValue().get("product:"  + productId);
            if (product != null) {
                return product;
            }
            product = productRepository.findById(productId).orElseThrow(()  -> new RuntimeException("商品不存在"));
            redisTemplate.opsForValue().set("product:"  + productId, product, -1, TimeUnit.SECONDS);
            return product;
        }
    }
}  

缓存与数据库的数据一致性

异步刷新

通过异步刷新机制,可以在不影响用户体验的情况下保持缓存与数据库的一致性。

@Service 
public class ProductService {
 
    @Autowired 
    private RedisTemplate<String, Object> redisTemplate;
 
    @Autowired 
    private ProductRepository productRepository;
 
    @Async 
    public void asyncRefreshCache(String productId) {
        Product product = productRepository.findById(productId).orElse(null); 
        if (product != null) {
            redisTemplate.opsForValue().set("product:"  + productId, product, 3600, TimeUnit.SECONDS);
        }
    }
}  

缓存更新策略

  • 写通策略:写数据库时同时更新缓存。
  • 读写策略:写数据库时先删除缓存,读时再加载。
  • 最终一致性:允许一定时间内的数据不一致。

写通策略实现

@Service 
public class ProductService {
 
    @Autowired 
    private RedisTemplate<String, Object> redisTemplate;
 
    @Autowired 
    private ProductRepository productRepository;
 
    public void updateProduct(Product product) {
        productRepository.save(product); 
        redisTemplate.opsForValue().set("product:"  + product.getId(),  product, 3600, TimeUnit.SECONDS);
    }
}  

容器化部署与缓存同步

在容器化部署场景下,可以通过 Redis 集群实现缓存同步。

  
# Redis 集群配置示例 
spring:
  redis:
    cluster:
      nodes: 192.168.1.1:6379,192.168.1.2:6379,192.168.1.3:6379   

案例分析:库存信息的缓存设计

场景描述

在秒杀场景中,库存信息是最容易成为性能瓶颈的部分。我们需要通过缓存设计来提升库存查询和扣减的效率。

方案设计

  • 使用 Redis 缓存库存信息。
  • 结合分布式锁实现库存扣减。
  • 使用异步刷新机制保持缓存与数据库的一致性。

代码实现

@Service 
public class StockService {
 
    @Autowired 
    private RedisTemplate<String, Integer> redisTemplate;
 
    @Autowired 
    private ProductRepository productRepository;
 
    public boolean decreaseStock(String productId, int num) {
        // 先尝试从 Redis 中扣减库存 
        Integer stock = redisTemplate.opsForValue().get("stock:"  + productId);
        if (stock != null && stock >= num) {
            redisTemplate.opsForValue().decrement("stock:"  + productId, num);
            return true;
        }
        // 如果 Redis 中没有库存或库存不足,从数据库中获取 
        Product product = productRepository.findById(productId).orElseThrow(()  -> new RuntimeException("商品不存在"));
        if (product.getStock()  < num) {
            return false;
        }
        // 扣减数据库中的库存 
        product.setStock(product.getStock()  - num);
        productRepository.save(product); 
        // 更新 Redis 中的库存 
        redisTemplate.opsForValue().set("stock:"  + productId, product.getStock(),  3600, TimeUnit.SECONDS);
        return true;
    }
}  

总结与展望

通过本文的讲解,我们已经了解了如何基于 Redis 实现一个高效、可靠的缓存层。在实际开发中,我们需要根据具体的业务需求选择合适的缓存策略,并通过不断的优化和测试提升系统的性能和稳定性。

在接下来的文章中,我们将继续深入探讨秒杀系统的其他核心模块,包括:

  • 消息队列配置与异步处理
  • 监控体系建设与性能分析
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号