电商库存扣减

原文地址 cloud.tencent.com

在日常开发中有很多地方都有类似扣减库存的操作,比如电商系统中的商品库存,抽奖系统中的奖品库存等。

在日常开发中有很多地方都有类似扣减库存的操作,比如电商系统中的商品库存,抽奖系统中的奖品库存等。

解决方案

  1. 使用 mysql 数据库,使用一个字段来存储库存,每次扣减库存去更新这个字段。
  2. 还是使用数据库,但是将库存分层多份存到多条记录里面,扣减库存的时候路由一下,这样子增大了并发量,但是还是避免不了大量的去访问数据库来更新库存。
  3. 将库存放到 redis 使用 redis 的 incrby 特性来扣减库存。

分析

在上面的第一种和第二种方式都是基于数据来扣减库存。

基于数据库单库存

第一种方式在所有请求都会在这里等待锁,获取锁有去扣减库存。在并发量不高的情况下可以使用,但是一旦并发量大了就会有大量请求阻塞在这里,导致请求超时,进而整个系统雪崩;而且会频繁的去访问数据库,大量占用数据库资源,所以在并发高的情况下这种方式不适用。

基于数据库多库存

第二种方式其实是第一种方式的优化版本,在一定程度上提高了并发量,但是在还是会大量的对数据库做更新操作大量占用数据库资源。

基于数据库来实现扣减库存还存在的一些问题:

1、用数据库扣减库存的方式,扣减库存的操作必须在一条语句中执行,不能先 selec 在 update,这样在并发下会出现超扣的情况。如:

update number set x=x-1 where x > 0


2、MySQL 自身对于高并发的处理性能就会出现问题,一般来说,MySQL 的处理性能会随着并发 thread 上升而上升,但是到了一定的并发度之后会出现明显的拐点,之后一路下降,最终甚至会比单 thread 的性能还要差。

3、当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢 InnoDB 行锁的问题,导致出现互相等待甚至死锁,从而大大降低 MySQL 的处理性能,最终导致前端页面出现超时异常。

基于 redis

针对上述问题的问题我们就有了第三种方案,将库存放到缓存,利用 redis 的 incrby 特性来扣减库存,解决了超扣和性能问题。但是一旦缓存丢失需要考虑恢复方案。比如抽奖系统扣奖品库存的时候,初始库存 = 总的库存数 - 已经发放的奖励数,但是如果是异步发奖,需要等到 MQ 消息消费完了才能重启 redis 初始化库存,否则也存在库存不一致的问题。

基于 redis 实现扣减库存的具体实现

  • 我们使用 redis 的 lua 脚本来实现扣减库存
  • 由于是分布式环境下所以还需要一个分布式锁来控制只能有一个服务去初始化库存
  • 需要提供一个回调函数,在初始化库存的时候去调用这个函数获取初始化库存

初始化库存回调函数 (IStockCallback)

public interface IStockCallback {

 
 int getStock();
}


扣减库存服务(StockService)

@Service
public class StockService {
    Logger logger = LoggerFactory.getLogger(StockService.class);

    
    public static final long UNINITIALIZED_STOCK = -3L;

    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    
    public static final String STOCK_LUA;

    static {
        
        StringBuilder sb = new StringBuilder();
        sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
        sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        sb.append("    local num = tonumber(ARGV[1]);");
        sb.append("    if (stock == -1) then");
        sb.append("        return -1;");
        sb.append("    end;");
        sb.append("    if (stock >= num) then");
        sb.append("        return redis.call('incrby', KEYS[1], 0 - num);");
        sb.append("    end;");
        sb.append("    return -2;");
        sb.append("end;");
        sb.append("return -3;");
        STOCK_LUA = sb.toString();
    }

    
    public long stock(String key, long expire, int num, IStockCallback stockCallback) {
        long stock = stock(key, num);
        
        if (stock == UNINITIALIZED_STOCK) {
            RedisLock redisLock = new RedisLock(redisTemplate, key);
            try {
                
                if (redisLock.tryLock()) {
                    
                    stock = stock(key, num);
                    if (stock == UNINITIALIZED_STOCK) {
                        
                        final int initStock = stockCallback.getStock();
                        
                        redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);
                        
                        stock = stock(key, num);
                    }
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                redisLock.unlock();
            }

        }
        return stock;
    }

    
    public long addStock(String key, int num) {

        return addStock(key, null, num);
    }

    
    public long addStock(String key, Long expire, int num) {
        boolean hasKey = redisTemplate.hasKey(key);
        
        if (hasKey) {
            return redisTemplate.opsForValue().increment(key, num);
        }

        Assert.notNull(expire,"初始化库存失败,库存过期时间不能为null");
        RedisLock redisLock = new RedisLock(redisTemplate, key);
        try {
            if (redisLock.tryLock()) {
                
                hasKey = redisTemplate.hasKey(key);
                if (!hasKey) {
                    
                    redisTemplate.opsForValue().set(key, num, expire, TimeUnit.SECONDS);
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        } finally {
            redisLock.unlock();
        }

        return num;
    }

    
    public int getStock(String key) {
        Integer stock = (Integer) redisTemplate.opsForValue().get(key);
        return stock == null ? -1 : stock;
    }

    
    private Long stock(String key, int num) {
        
        List<String> keys = new ArrayList<>();
        keys.add(key);
        
        List<String> args = new ArrayList<>();
        args.add(Integer.toString(num));

        long result = redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                Object nativeConnection = connection.getNativeConnection();
                
                
                if (nativeConnection instanceof JedisCluster) {
                    return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);
                }

                
                else if (nativeConnection instanceof Jedis) {
                    return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
                }
                return UNINITIALIZED_STOCK;
            }
        });
        return result;
    }

}


调用

@RestController
public class StockController {

    @Autowired
    private StockService stockService;

    @RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Object stock() {
        
        long commodityId = 1;
        
        String redisKey = "redis_key:stock:" + commodityId;
        long stock = stockService.stock(redisKey, 60 * 60, 2, () -> initStock(commodityId));
        return stock >= 0;
    }

    
    private int initStock(long commodityId) {
        
        return 1000;
    }

    @RequestMapping(value = "getStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Object getStock() {
        
        long commodityId = 1;
        
        String redisKey = "redis_key:stock:" + commodityId;

        return stockService.getStock(redisKey);
    }

    @RequestMapping(value = "addStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Object addStock() {
        
        long commodityId = 2;
        
        String redisKey = "redis_key:stock:" + commodityId;

        return stockService.addStock(redisKey, 2);
    }
}


参考

  • http://www.cnblogs.com/billyxp/p/3701124.html
  • http://blog.csdn.net/jiao_fuyou/article/details/15504777
  • https://www.jianshu.com/p/48c1a92fbf3a

本文分享自微信公众号 - 好好学 java(SIHAIloveJAVA)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-03-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值