Source: my.oschina.net/xiaolyuh/blog/1615639
There are many places in daily development that have similar operations to deduct inventory, such as commodity inventory in the e-commerce system, and prize inventory in the lottery system.
solution
- Using a mysql database, use a field to store inventory, and update this field each time the inventory is deducted.
- The database is still used, but the inventory is stored in multiple layers in multiple records, and the routing is done when the inventory is deducted. This increases the concurrency, but it still cannot avoid a lot of accessing the database to update the inventory.
- Putting inventory into redis uses the incrby feature of redis to deduct inventory.
analyze
The first and second methods above are based on data to deduct inventory.
Inventory based on database order
The first method will wait for the lock here in all requests, and acquire the lock to deduct the inventory. It can be used when the amount of concurrency is not high, but once the amount of concurrency is large, a large number of requests will be blocked here, causing the request to time out and the entire system to avalanche; and it will frequently access the database, occupying a lot of database resources, so in This method is not applicable in the case of high concurrency.
Multi-inventory based on database
The second method is actually an optimized version of the first method, which increases the amount of concurrency to a certain extent, but still a large number of database update operations will occupy a large amount of database resources.
There are still some problems in realizing inventory deduction based on database:
Using the database to deduct inventory, the operation of deducting inventory must be performed in one statement, and it is not possible to selec t update first, so that over-deduction will occur under concurrency. like:
update number set x=x-1 where x > 0
MySQL itself will have problems with high concurrency processing performance. Generally speaking, MySQL processing performance will increase with the increase of concurrent thread s, but after a certain degree of concurrency, there will be an obvious inflection point, and then it will decline all the way, and eventually even Even worse than single thread performance.
When inventory reduction and high concurrency come together, since the number of operating inventories is in the same row, there will be a problem of competing for InnoDB row locks, resulting in mutual waiting or even deadlock, which greatly reduces the processing performance of MySQL and eventually leads to The front-end page has a timeout exception.
based on redis
In response to 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, which solves the problem of over-deduction and performance. But once the cache is lost, a recovery plan needs to be considered. For example, when the lottery system deducts the inventory of prizes, the initial inventory = total inventory - the number of awards that have been issued, but if the award is issued asynchronously, you need to wait until the MQ message is consumed before restarting redis to initialize the inventory, otherwise there will also be inventory inconsistencies.
The specific implementation of deducting inventory based on redis
- We use redis' lua script to deduct inventory
- Because it is a distributed environment, a distributed lock is also required to control only one service to initialize the inventory
- You need to provide a callback function, call this function when initializing the inventory to get the initialized inventory
Initialize the inventory callback function (IStockCallback)
/** * Get Inventory Callback * @author yuhao.wang */ public interface IStockCallback { /** * Get stock * @return */ int getStock(); }
Recommend a Spring Boot basic tutorial and practical example:
https://github.com/javastacks/spring-boot-best-practice
Deduction Inventory Service (StockService)
/** * deduction of inventory * * @author yuhao.wang */ @Service public class StockService { Logger logger = LoggerFactory.getLogger(StockService.class); /** * Unlimited inventory */ public static final long UNINITIALIZED_STOCK = -3L; /** * Redis client */ @Autowired private RedisTemplate<String, Object> redisTemplate; /** * Execute the script to deduct inventory */ public static final String STOCK_LUA; static { /** * * @desc Deduct inventory Lua script * Inventory (stock)-1: Indicates unlimited inventory * stock (stock) 0: means no stock * Inventory (stock) greater than 0: indicates remaining inventory * * @params stock key * @return * -3:Inventory not initialized * -2:Inventory shortage * -1:Unlimited inventory * Greater than or equal to 0: remaining inventory (the remaining inventory after deduction) * redis The cached inventory (value) is -1, which means unlimited inventory, and returns 1 directly */ 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 stock key * @param expire Inventory valid time, in seconds * @param num Deduction amount * @param stockCallback Initialize inventory callback function * @return -2:Insufficient inventory; -1: unlimited inventory; greater than or equal to 0: remaining inventory after deducting inventory */ public long stock(String key, long expire, int num, IStockCallback stockCallback) { long stock = stock(key, num); // Initialize inventory if (stock == UNINITIALIZED_STOCK) { RedisLock redisLock = new RedisLock(redisTemplate, key); try { // acquire lock if (redisLock.tryLock()) { // Double verification to avoid repeated back-to-source to the database during concurrency stock = stock(key, num); if (stock == UNINITIALIZED_STOCK) { // Get initialized inventory final int initStock = stockCallback.getStock(); // set inventory to redis redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS); // Adjust the operation of deducting inventory once stock = stock(key, num); } } } catch (Exception e) { logger.error(e.getMessage(), e); } finally { redisLock.unlock(); } } return stock; } /** * Add Inventory (Restore Inventory) * * @param key stock key * @param num Inventory quantity * @return */ public long addStock(String key, int num) { return addStock(key, null, num); } /** * add stock * * @param key stock key * @param expire Expiration time (seconds) * @param num Inventory quantity * @return */ public long addStock(String key, Long expire, int num) { boolean hasKey = redisTemplate.hasKey(key); // Determine whether the key exists, and update it directly if it exists if (hasKey) { return redisTemplate.opsForValue().increment(key, num); } Assert.notNull(expire,"Failed to initialize inventory, inventory expiration time cannot be null"); RedisLock redisLock = new RedisLock(redisTemplate, key); try { if (redisLock.tryLock()) { // After obtaining the lock, check again whether there is a key hasKey = redisTemplate.hasKey(key); if (!hasKey) { // Initialize inventory redisTemplate.opsForValue().set(key, num, expire, TimeUnit.SECONDS); } } } catch (Exception e) { logger.error(e.getMessage(), e); } finally { redisLock.unlock(); } return num; } /** * Get stock * * @param key stock 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; } /** * deduction of inventory * * @param key stock key * @param num Deduct inventory quantity * @return Remaining inventory after deduction [-3: inventory not initialized; -2: insufficient inventory; -1: unlimited inventory; greater than or equal to 0: remaining inventory after deduction of inventory] */ private Long stock(String key, int num) { // KEYS parameter in script List<String> keys = new ArrayList<>(); keys.add(key); // ARGV parameters in scripts 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(); // Although the method of executing scripts in cluster mode and stand-alone mode is the same, there is no common interface, so they can only be executed separately // cluster mode if (nativeConnection instanceof JedisCluster) { return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args); } // Standalone mode else if (nativeConnection instanceof Jedis) { return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args); } return UNINITIALIZED_STOCK; } }); return result; } }
transfer
/** * @author yuhao.wang */ @RestController public class StockController { @Autowired private StockService stockService; @RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Object stock() { // Product 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; } /** * Get initial inventory * * @return */ private int initStock(long commodityId) { // TODO here do some operations to initialize the inventory return 1000; } @RequestMapping(value = "getStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Object getStock() { // Product 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() { // Product ID long commodityId = 2; // Inventory ID String redisKey = "redis_key:stock:" + commodityId; return stockService.addStock(redisKey, 2); } }
Recommended recent hot articles:
1.1,000+ Java interview questions and answers (2022 latest version)
2.Awesome! Java coroutines are coming. . .
3.Spring Boot 2.x tutorial, so complete!
4.Don't write full screen explosions, try the decorator mode, this is the elegant way! !
5.The latest release of "Java Development Manual (Songshan Edition)", download quickly!
If you think it's good, don't forget to like + retweet!