In daily development, there are many similar operations to reduce inventory, such as commodity inventory in e-commerce system, prize inventory in lottery system, etc.

The solution

With mysql database, use a field to store inventory and update this field each time inventory is deducted. The database is still used, but the inventory is layered and stored in multiple records, routing when inventory is deducted, which increases the concurrency, but still can not avoid a large number of visits to the database to update the inventory. Put inventory to Redis Use the Incrby feature of Redis to deduct inventory.

Analysis of the

The first and second methods above are based on data to deduct inventory.

Based on database single inventory

The first way is that all requests will wait here for the lock, and acquiring the lock has to detract from the inventory. It can be used when the number of concurrent requests is not high, but when the number of concurrent requests is high, a large number of requests will be blocked, resulting in request timeout, and the whole system avalanche; In addition, the database will be frequently accessed, occupying a large number of database resources, so this method is not applicable in the case of high concurrency.

Multiple inventory based database

The second way is actually the optimized version of the first way, which improves the concurrency to a certain extent, but still consumes a lot of database resources by doing a lot of update operations to the database.

There are still some problems in realizing inventory deduction based on database:

In the way of database inventory deduction, the operation of inventory deduction must be executed in one statement, not selec before update, so that the situation of overdeduction will occur in concurrent. Update number set x=x-1 where x > 0

In general, the processing performance of MySQL will improve with the increase of concurrent threads. However, after a certain degree of concurrency, there will be an obvious inflection point, and then it will decline all the way, and finally it will be worse than the performance of single threads.

When inventory reduction and high concurrency meet together, because the number of inventory operation is in the same row, there will be the problem of fighting for InnoDB row lock, resulting in mutual waiting or even deadlock, which greatly reduces the processing performance of MySQL, and ultimately lead to the front-end page timeout exception.

Based on the redis

In view of the above problems, we have a third solution, which is to put the inventory in the cache and use the Incrby feature of Redis to deduct the inventory, thus solving the overdeduction and performance problems. However, if the cache is lost, recovery plans need to be considered. For example, when the prize inventory is deducted by the lottery system, the initial inventory = total inventory – the number of awards that have been issued, but if the lottery is asynchronous, you need to wait until the MQ message consumption is finished before restarting the redis initialization inventory, otherwise there will be an inventory inconsistency problem.

Based on REDis to achieve the specific realization of inventory reduction

We use the Lua script of Redis to subtract inventory and since we’re in a distributed environment we also need a distributed lock to control that there’s only one service that initializes inventory and we need to provide a callback function that gets initialized inventory when we initialize inventory

Initialize the inventory callback function (IStockCallback)

@author yuhao.wang */ public interface IStockCallback {/** * return */ int getStock(); }Copy the code

StockService Deduction

/** * @author yuhao.wang */ @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 {/** ** @desc subtract inventory Lua script * stock -1: no inventory * stock 0: no inventory * stock greater than 0: Represents the remaining inventory * * @params inventory Key * @return * -3: inventory is not initialized * -2: inventory is insufficient * -1: unlimited inventory * Greater than or equal to 0: inventory remaining (inventory remaining after deduction) * Redis cache inventory (value is -1 means unlimited inventory, Return 1 */ 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(); } /** * @param key inventory key * @param expire inventory validity time, in seconds * @param num number of deductions * @param stockCallback initializes the inventory callback function * @return -2: Insufficient inventory; -1: Unlimited inventory; */ 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 {// get the lock if (redislock. tryLock()) {stock = stock(key, num); Final int initStock = stockcallback.getstock (); if (stock == UNINITIALIZED_STOCK) {final int initStock = stockcallback.getstock (); / / set the inventory to redis redisTemplate. OpsForValue (). The set (key, initStock, expire, TimeUnit. SECONDS); Stock = stock(key, num); } } } catch (Exception e) { logger.error(e.getMessage(), e); } finally { redisLock.unlock(); } } return stock; } /** * @param key * @param num * @return */ public long addStock(String key, String key) int num) { return addStock(key, null, num); } /** * add inventory ** @param key inventory * @param expire time (s) * @param num inventory * @return */ public long addStock(String key, String key)  Long expire, int num) { boolean hasKey = redisTemplate.hasKey(key); / / judge the existence of the key and direct updates if there's a (hasKey) {return redisTemplate. OpsForValue (). The increment (key, num); } assert. notNull(expire," failed to initialize inventory, inventory expiration cannot be null"); RedisLock redisLock = new RedisLock(redisTemplate, key); Try {if (redislock. tryLock()) {// If (redislock. tryLock()) = redistemplate.haskey (key); if (! HasKey) {/ / initial inventory redisTemplate opsForValue (). The set (key, num, expire, TimeUnit. SECONDS); } } } catch (Exception e) { logger.error(e.getMessage(), e); } finally { redisLock.unlock(); } return num; } /** * get inventory ** @param key inventory key * @return-1: unlimited inventory; Greater than or equal to 0: remaining inventory * / public int getStock (String key) {Integer stock = (Integer) redisTemplate. OpsForValue () get (key); return stock == null ? -1 : stock; } /** ** inventory ** @param key inventory key * @param num deduction inventory number * @return deduction inventory remaining [-3: inventory is not initialized; -2: inventory is insufficient; -1: unlimited inventory; */ 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(); // Cluster mode and single-machine mode execute scripts in the same way, but have no common interface. 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; }}Copy the code

call

/** * @author yuhao.wang */ @RestController public class StockController { @Autowired private StockService stockService;  @RequestMapping(value = "stock", Produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Object stock() {// ID long commodityId = 1; // Inventory ID String redisKey = "redis_key:stock:" + commodityId; long stock = stockService.stock(redisKey, 60 * 60, 2, () -> initStock(commodityId)); return stock >= 0; } @return */ private int initStock(long commodityId) {return 1000; } @RequestMapping(value = "getStock", Produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Object getStock() {// ID long commodityId = 1; // Inventory ID String redisKey = "redis_key:stock:" + commodityId; return stockService.getStock(redisKey); } @RequestMapping(value = "addStock", Produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Object addStock() {// ID long commodityId = 2; // Inventory ID String redisKey = "redis_key:stock:" + commodityId; return stockService.addStock(redisKey, 2); }}Copy the code