Java高手进阶:秒杀场景下的库存一致性解决方案

数据库 其他数据库
悲观锁机制通过锁定数据库中的某行数据,确保在高并发情况下只有一个用户可以修改库存。在用户请求秒杀时,数据库会锁定库存行,直到操作完成后才释放锁。

秒杀活动作为电商平台的重要营销手段,对库存管理的精确性提出了极高要求。防止超卖,即确保商品在秒杀过程中库存不会被过度消耗,是秒杀功能实现的关键。本文将探讨几种防止超卖的经典方案。

1.悲观锁机制

悲观锁机制通过锁定数据库中的某行数据,确保在高并发情况下只有一个用户可以修改库存。在用户请求秒杀时,数据库会锁定库存行,直到操作完成后才释放锁。

优缺点

  • 优点:强一致性保障,确保在高并发下不会出现超卖问题。
  • 缺点:锁的开销较大,容易导致数据库性能瓶颈。在高并发场景下,过多的悲观锁可能导致锁等待和死锁问题。

使用场景适用于对一致性要求极高,但并发量相对较小的场景。

Demo

@Mapper
public interface ProductMapper {
    @Select("SELECT stock FROM products WHERE product_id = #{productId} FOR UPDATE")
    Integer selectStockForUpdate(@Param("productId") int productId);
    @Update("UPDATE products SET stock = stock - 1 WHERE product_id = #{productId}")
    int updateStock(@Param("productId") int productId);
}
@Service
public class ProductService {
    @Autowired
    private ProductMapper productMapper;
    @Transactional
    public boolean seckill(int productId) {
        Integer stock = productMapper.selectStockForUpdate(productId);
        if (stock != null && stock > 0) {
            productMapper.updateStock(productId);
            return true; // 秒杀成功
        } else {
            return false; // 库存不足
        }
    }
}

2.乐观锁机制

乐观锁通常通过“版本号”机制来实现。在库存表中增加一个version字段,每次更新库存时,检查version是否与上次读取的一致,如果一致则更新库存和version,如果不一致则说明库存已经被其他用户修改过,需要重新尝试。

优缺点

  • 优点:无需锁表,对数据库性能影响较小,适合中小规模并发。
  • 缺点:并发过高时可能导致更新失败频繁,用户体验下降。在高并发场景下,乐观锁可能导致大量重试,增加系统负担。

使用场景适合于高并发但冲突不频繁的场景。

Demo

// 假设有一个Product实体类,包含stock和version字段
// 在Service层进行库存更新操作
@Service
public class ProductService {
    @Autowired
    private ProductMapper productMapper;
    @Transactional
    public boolean updateStock(int productId, int version) {
        Product product = productMapper.selectByPrimaryKey(productId);
        if (product.getVersion() != version) {
            return false; // 版本不匹配,更新失败
        }
        product.setStock(product.getStock() - 1);
        product.setVersion(product.getVersion() + 1);
        productMapper.updateByPrimaryKey(product);
        return true; // 更新成功
    }
}

3.分布式锁

分布式锁可以确保在多台服务器上并发处理库存时不会导致超卖。常用Redis来实现分布式锁。当用户请求秒杀时,先尝试通过Redis获得锁,如果获得锁则执行扣减库存操作,并释放锁;如果未获得锁则等待或重试。

优缺点

  • 优点:适合大规模并发场景,锁机制能够确保多台服务器在并发情况下安全修改库存。
  • 缺点:如果Redis出现故障,可能会影响锁的管理和库存的正确性。锁的粒度要控制好,锁的过大可能影响性能。在高并发场景下,分布式锁可能导致网络延迟和锁竞争问题。

使用场景适用于高并发场景,特别是多台服务器分布式部署时,对一致性要求较高。

Demo

@Service
public class SeckillService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    public boolean seckill(int productId) {
        String lockKey = "lock:product:" + productId;
        boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 5, TimeUnit.SECONDS);
        if (lock) {
            try {
                String stockKey = "stock:" + productId;
                Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
                if (stock != null && stock > 0) {
                    redisTemplate.opsForValue().decrement(stockKey);
                    return true; // 秒杀成功
                } else {
                    return false; // 库存不足
                }
            } finally {
                redisTemplate.delete(lockKey); // 释放锁
            }
        } else {
            return false; // 秒杀失败,未获得锁
        }
    }
}

4.库存预减+异步处理

在用户请求秒杀时,使用缓存进行库存预减(即在用户下单前就先减少库存),然后通过异步队列(如Kafka或RabbitMQ)将订单请求发往后端进行异步处理。商品秒杀开始前,将商品库存预先加载到Redis。当用户请求秒杀时,先从Redis中预减库存,将请求通过消息队列发送到后端进行订单处理和库存的最终确认。如果订单处理失败(如支付失败等),则通过异步任务将Redis中的库存回补。

优缺点

  • 优点:使用缓存大幅减少数据库压力,适合大规模并发场景。削峰填谷,利用消息队列将订单处理异步化,缓解高并发对数据库的冲击。
  • 缺点:需要处理订单失败后的库存回补,增加了系统复杂性。在极端情况下可能出现Redis库存与数据库库存不一致的问题,需要通过补偿机制来解决。同时,对缓存的可靠性和一致性要求较高。

使用场景适用于高并发场景,特别是需要减轻数据库压力时,对性能要求较高,但对一致性要求可以容忍一定延迟的场景。

Demo(仅展示库存预减部分,异步处理部分需结合消息队列实现):

@Service
public class SeckillService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    public boolean preDecreaseStock(int productId) {
        String stockKey = "stock:" + productId;
        Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
        if (stock != null && stock > 0) {
            redisTemplate.opsForValue().decrement(stockKey);
            return true; // 库存预减成功
        } else {
            return false; // 库存不足
        }
    }
}

5.小结

以上四种方案各有优劣,选择合适的方法取决于业务需求和技术栈。在实际应用中,可以根据并发量、系统性能要求、一致性需求等因素综合考虑选择合适的方案。

责任编辑:武晓燕 来源: JAVA充电
相关推荐

2023-05-09 10:59:33

缓存技术派MySQL

2021-06-06 12:45:41

分布式CAPBASE

2010-05-24 11:35:11

WCDMA

2023-06-29 08:00:59

redis数据MySQL

2024-10-08 10:10:00

削峰高并发流量

2023-06-07 08:10:29

2021-01-21 07:34:16

分布式系统场景

2017-07-25 14:38:56

数据库一致性非锁定读一致性锁定读

2023-11-01 10:11:00

Java分布式

2020-11-02 07:09:24

缓存服务器异构

2022-08-29 08:38:00

事务一致性

2022-12-14 08:23:30

2021-02-02 12:40:50

哈希算法数据

2021-02-05 08:00:48

哈希算法​机器

2023-08-01 07:42:33

Redis数据项目

2022-03-11 21:35:57

Java程序线程

2021-02-04 06:30:26

Python编程语言

2024-12-11 09:16:38

2019-11-21 10:19:45

数据应用场景系统

2016-12-19 18:41:09

哈希算法Java数据
点赞
收藏

51CTO技术栈公众号