redis?lua腳本解決高并發(fā)下秒殺場景
Redis lua腳本解決搶購秒殺場景
秒殺搶購可以說是在分布式環(huán)境下?個?常經(jīng)典的案例,?邊有很多痛點:
1.?并發(fā): 時間極短、瞬間?戶量?,?瞬間的?QPS把系統(tǒng)或數(shù)據(jù)庫直接打死,響應(yīng)失敗,導(dǎo)致與這個系統(tǒng)耦合的系統(tǒng)也GG
目前秒殺的實現(xiàn)方案主要有兩種:
2.超賣: 你只有?百件商品,由于是?并發(fā)的問題,導(dǎo)致超賣的情況
目前秒殺的實現(xiàn)方案主要有兩種:
1.用redis 將搶購信息進(jìn)行存儲。然后再慢慢消費(fèi)。 同時,服務(wù)器給與用戶快速響應(yīng)。
2.用mq實現(xiàn),比如RabbitMQ,服務(wù)器將請求過來的數(shù)據(jù)先讓RabbitMQ存起來,然后再慢慢消費(fèi)掉。
也可以結(jié)合redis與mq的方式,通過redis控制剩余庫存,達(dá)到快速響應(yīng),將滿足條件的購買的訂單先讓RabbitMQ存起來,后續(xù)在慢慢消化。
整體流程
1.服務(wù)器接收到了大量用戶請求過來(1s 2000個請求)。比如傳了用戶信息,產(chǎn)品信息,和購買數(shù)量信息。此時 服務(wù)器采用redis 的lua 腳本 去調(diào)用redis 中間件。lua 腳本的邏輯是減庫存,校驗庫存是否足夠。然后迅速給與服務(wù)器反饋(庫存是否夠,夠返回 1 ,不夠返回 0)。
2.服務(wù)器迅速給與用戶的請求反饋。提示搶購成功.或者搶購失敗
3.搶購成功,將訂單信息放入MQ,其余線程接受到MQ的信息后,將訂單信息存入DB中
4.后面客戶就可以查詢 mysql 的訂單信息了。
架構(gòu)
采用springboot+redis+mysql+myBatis.
數(shù)據(jù)庫
CREATE TABLE `tb_product` ( `id` bigint NOT NULL AUTO_INCREMENT, `product_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'id', `price` decimal(65,18) NOT NULL DEFAULT '0', `available_qty` bigint NOT NULL DEFAULT '0' COMMENT '發(fā)行數(shù)量', `title` varchar(1024) NOT NULL DEFAULT '', `end_time` bigint NOT NULL DEFAULT '0', `start_time` bigint NOT NULL DEFAULT '0', `created` bigint NOT NULL DEFAULT '0', `updated` bigint NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `uq_product_id` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
pom依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
lua 腳本
1.減少庫存,校驗庫存是否充足
2.庫存數(shù)量回滾:
核心業(yè)務(wù)代碼展示
1.加載lua腳本
private final static DefaultRedisScript<Long> deductRedisScript = new DefaultRedisScript(); private final static DefaultRedisScript<Long> increaseRedisScript = new DefaultRedisScript(); //加載lua腳本 @PostConstruct void init() { //加載削減庫存lua腳本 deductRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/fixedDeductInventory.lua"))); deductRedisScript.setResultType(Long.class); //加載庫存回滾lua腳本 increaseRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/fixedIncreaseInventory.lua"))); increaseRedisScript.setResultType(Long.class); }
2.添加庫存到redis
注意點:在使用redis集群時,lua腳本中存在多個key時,可以通過hash tag
這個方法將不同key的值落在同一個槽位上,hash tag 是通過{}
這對括號括起來的字符串,如果下列中{fixed:" + data.getProductId() + "} 作為tag,確保同一個產(chǎn)品的信息都在同一個槽位。
@Resource(name = "fixedCacheRedisTemplate") private RedisTemplate<String, Long> fixedCacheRedisTemplate; public void ProductToOngoing(Product data, Long time) { //設(shè)置數(shù)量 long number = data.getAvailableQty(); fixedCacheRedisTemplate.opsForHash().putIfAbsent("{fixed:" + data.getProductId() + "}-residue_stock_" + data.getRecordId(), "{fixed:" + data.getProductId() + "}-residueStock" , number); String statusKey = "fixed_product_sold_status_"+ data.getRecordId(); long timeout = data.getEndTime() - data.getStartTime(); //添加產(chǎn)品出售狀態(tài) fixedCacheRedisTemplate.opsForValue().set(statusKey, 1L, data.getEndTime() - data.getStartTime(), TimeUnit.MILLISECONDS); }
3.下單&庫存校驗
//檢查庫存 public boolean checkFixedOrderQty(Long userId, Long productId, Long quantity, Long overTime) { Boolean pendingOrder = false; String userKey = ""; try { //校驗是否開始 String statusKey = "fixed_product_sold_status_" + productId; Long fixedStartStatus = fixedCacheRedisTemplate.opsForValue().get(statusKey); if (fixedStartStatus == null || fixedStartStatus != 1L) { //報錯返回,商品未開售 throw new WebException(ResultCode.SALE_HAS_NOT_START); } //檢查庫存數(shù)量 Long number = deductInventory(productId, quantity); if (number != 1L) { log.warn("availbale num is null:{} {}", productId, number); throw new WebException(ResultCode.AVAILABLE_AMOUNT_INSUFFICIENT); } return true; } catch (Exception e) { log.warn("checkFixedOrderQty error:{}", e.getMessage(), e); throw e; } } //下單 public void createOrder(Long userId, Long productId, BigDecimal price, Long quantity){ boolean check = checkFixedOrderQty(userId, productId, quantity); try { if (check) { //添加MQ等待下單,后續(xù)收到推送的線程保存靠DB中 CreateCoinOrderData data = new CreateCoinOrderData(); data.setUserId(userId); data.setProductId(productId); data.setPrice(price); data.setQuantity(quantity); rabbitmqProducer.sendMessage(1, JSONObject.toJSONString(data)); } } catch (Exception e) { //發(fā)生異常,庫存需要回滾 increaseInventory(recordId, quantity, 1L); throw e; } } //庫存回填 public Long increaseInventory(Long productId, Long num) { try { // 構(gòu)建keys信息,代表hash值中所需要的key信息 List<String> keys = Arrays.asList("{fixed:" + productId + "}-residue_stock_"+ recordId, "{fixed:" + productId + "}-residueStock"); // 執(zhí)行腳本 Object result = fixedCacheRedisTemplate.execute(increaseRedisScript, keys, num); log.info("increaseInventory productId :{} num:{} result:{}", productId, num, result); return (Long) result; } catch (Exception e) { log.warn("increaseInventory error productId:{} num:{}", productId, num); } return 0L; }
以上就是redis lua腳本解決高并發(fā)下秒殺場景的詳細(xì)內(nèi)容,更多關(guān)于redis lua高并發(fā)秒殺的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
redis發(fā)布和訂閱_動力節(jié)點Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了redis發(fā)布和訂閱的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08如何高效地向Redis插入大量的數(shù)據(jù)(推薦)
本篇文章主要介紹了如何高效地向Redis插入大量的數(shù)據(jù),現(xiàn)在分享給大家,感興趣的小伙伴們可以參考一下。2016-11-11