需求
雙十二要搞一個一分錢門票搶購的活動。
分析
性能分析,搶購時會發生高並發,如果僅僅依靠Mysql數據庫,有可能因為大量的請求頻繁訪問數據庫造成服務器雪崩,所以考慮通過Redis減庫存,最終的數據落地到DB中。
在高並發的情況下,還要考慮到超賣的問題,因而打算使用Lua腳本完成原子減的操作。
在這里,我們只針對減庫存的操作進行分析。
實現
不使用原子操作,出現超賣的情況。第一步:先從redis中查出庫存進行判斷,第二步:如果庫存>0,則進行減庫存的操作。
代碼實現:
1 // 第一步:從redis中查出庫存 2 Integer stock = (Integer) RedisUtils.get("stock"); 3 4 // 第二步:如果庫存>0,則進行減庫存的操作 5 if (stock > 0) { 6 long spareStock = RedisUtils.decr("stock", 1); 7 System.out.println(getName() + "搶到了第" + spareStock + "件"); 8 } else { 9 System.out.println("庫存不足"); 10 }
用多線程模擬並發請求:庫存為500,創建505個線程去搶購。
1 for(int i =1;i<=505;i++){ 2 MyThread2 thread =new MyThread2("線程"+i); 3 thread.start(); 4 }
執行結果:出現超賣問題,原因是:查詢庫存及減庫存不是原子性操作。
使用原子性操作:直接減庫存。
1 public void run() { 2 long stock = RedisUtils.stock("stock"); 3 if (stock > 0) { 4 System.out.println(getName() + "搶到了第" + stock + "件"); 5 } else { 6 System.out.println("庫存不足"); 7 } 8 9 }
Lua腳本實現減庫存操作:
/** * 庫存不足 */ public static final int LOW_STOCK = 0; /** * 不限庫存 */ public static final long UNINITIALIZED_STOCK = -1L; /** * 執行扣庫存的腳本 */ public static final String STOCK_LUA; static { // 初始化減庫存lua腳本 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(" if (stock == -1) then"); sb.append(" return 1;"); sb.append(" end;"); sb.append(" if (stock > 0) then"); sb.append(" redis.call('incrby', KEYS[1], -1);"); sb.append(" return stock;"); sb.append(" end;"); sb.append(" return 0;"); sb.append("end;"); sb.append("return -1;"); STOCK_LUA = sb.toString(); } /** * 扣庫存 * * @param key 庫存key * @return 扣減之前剩余的庫存【0:庫存不足; -1:庫存未初始化; 大於0:扣減庫存之前的剩余庫存】 */ public static Long stock(String key) { // 腳本里的KEYS參數 List<String> keys = new ArrayList<>(); keys.add(key); // 腳本里的ARGV參數 List<String> args = new ArrayList<>(); Long result = (Long)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; }
執行結果:505個線程去搶500個商品,有五個線程會搶不到,測試結果與預期一致,解決了超賣的問題。
參考:https://blog.csdn.net/xiaolyuh123/article/details/79208959