- 功能核心點
* 經典互聯網商品搶購秒殺功能
- 功能api
* 商品秒殺接口
- 數據落地存儲方案
* 通過分布式redis減庫存
* DB存最終訂單信息數據
- api性能調優
* 性能瓶頸在高並發秒殺
* 技術難題在於超賣問題
秒殺系統功能步驟梳理
- 利用 Redis 緩存incr攔截流量
- 首先通過數據控制模塊,提前將秒殺商品緩存到讀寫分離 Redis,並設置秒殺開始標記如下:
"skuId_start": 0 //開始標記1表示秒殺開始
"skuId_count": 10000 //總數
"skuId_access": 12000 //接受搶購數(接受最大搶購數=1.2*商品總數)
- 秒殺開始前,服務集群讀取 skuId_start為 0,直接返回未開始。
- 服務時間不一致可能導致流量傾斜
- 數據控制模塊將 skuId_start 改為1,標志秒殺開始。
- 當接受下單數達到 sku_count*1.2 后,繼續攔截所有請求,商品剩余數量為 0
- 利用Redis緩存加速庫存扣量
"skuId_booked": 10000 //總數0開始10000 通過incr扣減庫存,返回搶購成功
- 將用戶訂單數據寫入mq
- 監聽mq入庫
秒殺系統功能api實戰(上)
** 后端秒殺網關流量攔截層功能開發 **
- 先判斷秒殺是否已經開始
* 初始化時將key:skuId_start_1 value:0_1554046102存入數據庫中
- 利用 Redis 緩存incr攔截流量
- 緩存攔截流量代碼編寫
- 用incr方法原子加
- 通過原子加判斷當前skuId_access是否達到最大值
- 思考:是否需要保證獲取到值的時候和incr值兩個命令的原子性
* 保證原子性的方式,采用lua腳本
* 采用lua腳本方式保證原子性帶來缺點,性能有所下降
* 不保證原子性缺點,放入請求量可能大於skuId_access
秒殺系統功能api實戰(中)
** 后端秒殺信息校驗層功能開發布隆過濾器實現重復購買攔截 **
- 訂單信息校驗層
* 校驗當前用戶是否已經買過這個商品
- 需要存儲用戶的uid
- 存數據庫效率太低
- 存Redis value方式數據太大
- 存布隆過濾器性能高且數據量小
- 校驗完通過直接返回搶購成功
秒殺系統功能api實戰(下)
** 后端秒殺信息校驗層功能開發lua腳本實現庫存扣除**
- 庫存扣除成功,獲取當前最新庫存
- 如果庫存大於0,即馬上進行庫存扣除,並且訪問搶購成功給用戶
- 考慮原子性問題
* 保證原子性的方式,采用lua腳本
* 采用lua腳本方式保證原子性帶來缺點,性能有所下降
* 不保證原子性缺點,放入請求量可能大於預期值
* 當前扣除庫存場景必須保證原子性,否則會導致超賣
- 返回搶購結果
* 搶購成功
* 庫存沒了 ,搶購失敗
初始化庫存數據:
set skuId_count_1 10000 set skuId_access_1 12000 set skuId_start_1 0_1571125152
set skuId_booked_1 0
代碼:
package com.concurrent.service; import com.concurrent.util.RedisService; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class SeckillService { private static final String secStartPrefix = "skuId_start_"; private static final String secAccess = "skuId_access_"; private static final String secCount = "skuId_count_"; private static final String filterName = "skuId_bloomfilter_"; private static final String bookedName = "skuId_booked_"; @Resource private RedisService redisService; public String seckill(int uid, int skuId) { //流量攔截層 //1、判斷秒殺是否開始 0_1554045087 開始標識_開始時間 String isStart = (String) redisService.get(secStartPrefix + skuId); if (StringUtils.isBlank(isStart)) { return "還未開始"; } if (isStart.contains("_")) { Integer isStartInt = Integer.parseInt(isStart.split("_")[0]); Integer startTime = Integer.parseInt(isStart.split("_")[1]); if (isStartInt == 0) { if (startTime > getNow()) { return "還未開始"; } else { //代表秒殺已經開始 redisService.set(secStartPrefix + skuId, 1+""); } } else { return "系統異常"; } } else { if (Integer.parseInt(isStart) != 1) { return "系統異常"; } } //流量攔截 String skuIdAccessName = secAccess + skuId; Integer accessNumInt = 0; String accessNum = (String) redisService.get(skuIdAccessName); if(StringUtils.isNotBlank(accessNum)){ accessNumInt = Integer.parseInt(accessNum); } String skuIdCountName = secCount + skuId; Integer countNumInt = Integer.parseInt((String) redisService.get(skuIdCountName)); if (countNumInt * 1.2 < accessNumInt) { return "搶購已經完成,歡迎下次參與"; } else { redisService.incr(skuIdAccessName); } //信息校驗層 if (redisService.bloomFilterExists(filterName + skuId, uid)){ return "您已經搶購過該商品,請勿重復下發!"; }else{ Boolean isSuccess = redisService.getAndIncrLua(bookedName+skuId,skuIdCountName); if(isSuccess){ redisService.bloomFilterAdd(filterName + skuId, uid); //TODO 放入消息隊列進行異步入庫 return "恭喜您搶購成功!!!"; }else{ return "搶購結束,歡迎下次參與"; } } } private long getNow() { return System.currentTimeMillis() / 1000; } }
RedisService.java
package com.concurrent.util; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.*; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.scripting.support.ResourceScriptSource; import org.springframework.stereotype.Service; import java.io.InterruptedIOException; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Created by Administrator on 2018/10/6. */ @Service public class RedisService { @Autowired private RedisTemplate redisTemplate; private static double size = Math.pow(2, 32); private static final String bloomFilterName = "isVipBloom"; /** * * * @param key * @return */ public Object get(final String key) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); return operations.get(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 自增 * * @param key * @return */ public boolean incr(final String key) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.increment(key,1); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } public Boolean bloomFilterAdd(int value){ DefaultRedisScript<Boolean> bloomAdd = new DefaultRedisScript<>(); bloomAdd.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterAdd.lua"))); bloomAdd.setResultType(Boolean.class); List<Object> keyList= new ArrayList<>(); keyList.add(bloomFilterName); keyList.add(value+""); Boolean result = (Boolean) redisTemplate.execute(bloomAdd,keyList); return result; } public Boolean bloomFilterAdd(String bloomName,int value){ DefaultRedisScript<Boolean> bloomAdd = new DefaultRedisScript<>(); bloomAdd.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterAdd.lua"))); bloomAdd.setResultType(Boolean.class); List<Object> keyList= new ArrayList<>(); keyList.add(bloomName); keyList.add(value+""); Boolean result = (Boolean) redisTemplate.execute(bloomAdd,keyList); return result; } public Boolean bloomFilterExists(int value) { DefaultRedisScript<Boolean> bloomExists = new DefaultRedisScript<>(); bloomExists.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterExist.lua"))); bloomExists.setResultType(Boolean.class); List<Object> keyList = new ArrayList<>(); keyList.add(bloomFilterName); keyList.add(value + ""); Boolean result = (Boolean) redisTemplate.execute(bloomExists, keyList); return result; } public Boolean bloomFilterExists(String bloomName,int value) { DefaultRedisScript<Boolean> bloomExists = new DefaultRedisScript<>(); bloomExists.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterExist.lua"))); bloomExists.setResultType(Boolean.class); List<Object> keyList = new ArrayList<>(); keyList.add(bloomName); keyList.add(value + ""); Boolean result = (Boolean) redisTemplate.execute(bloomExists, keyList); return result; }
public Boolean getAndIncrLua(String key,String key2){
DefaultRedisScript<Boolean> bloomExists= new DefaultRedisScript<>();
bloomExists.setScriptSource(new ResourceScriptSource(new ClassPathResource("secKillIncr2.lua")));
bloomExists.setResultType(Boolean.class);
List<Object> keyList= new ArrayList<>();
keyList.add(key);
keyList.add(key2);
Boolean result = (Boolean) redisTemplate.execute(bloomExists,keyList);
return result;
}
}
secKillIncr.lua腳本:
local lockKey = KEYS[1]
local lockKey2 = KEYS[2]
-- get info
local result_1 = redis.call('GET', lockKey)
local maxCount = redis.call('GET', lockKey2)
if tonumber(result_1) < tonumber(maxCount)
then
local result_2= redis.call('INCR', lockKey)
return result_1
else
return 0
end
controller:
package com.concurrent.controller; import com.concurrent.SecondKillApplication; import com.concurrent.service.SeckillService; import org.springframework.boot.SpringApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class SeckillController { @Resource private SeckillService seckillService; @RequestMapping("/redis/seckill") public String secKill(int uid,int skuId){ return seckillService.seckill(uid,skuId); } public static void main(String[] args) { System.out.println(System.currentTimeMillis()/1000); } }