如何設計一個秒殺系統
一、秒殺介面優化思路:
1、系統初始化把商品數量載入到redis
2、收到請求redis預減庫存,庫存不足直接返回,否則進入3
3、後端請求進入mq佇列,前端顯示請求中
4、請求出隊,生成訂單,減少庫存
5、客戶端輪詢,是否秒殺成功
二、秒殺介面的隱藏:秒殺開始之前先去請求獲取秒殺的地址
1、介面改造帶上PathVariable引數
2、新增生成地址的介面
3、秒殺收到請求先驗證PathVariable
三、圖形驗證碼分散使用者請求:
1、新增生成驗證碼的介面
2、在獲取秒殺路徑的時候,驗證驗證碼
3、ScriptEngine的使用
四、介面防刷:用redis對介面限流
後端主要程式碼:
@Controller
@RequestMapping("/miaosha")
public class MiaoshaController implements InitializingBean {
@Autowired
MiaoshaUserService userService;
@Autowired
RedisService redisService;
@Autowired
GoodsService goodsService;
@Autowired
OrderService orderService;
@Autowired
MiaoshaService miaoshaService;
@Autowired
MQSender sender;
private HashMap<Long, Boolean> localOverMap = new HashMap<Long, Boolean>();
/**
* 系統初始化
* */
public void afterPropertiesSet() throws Exception {
List<GoodsVo> goodsList = goodsService.listGoodsVo();
if(goodsList == null) {
return;
}
for(GoodsVo goods : goodsList) {
redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
localOverMap.put(goods.getId(), false);
}
}
@RequestMapping(value="/reset", method=RequestMethod.GET)
@ResponseBody
public Result<Boolean> reset(Model model) {
List<GoodsVo> goodsList = goodsService.listGoodsVo();
for(GoodsVo goods : goodsList) {
goods.setStockCount(10);
redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), 10);
localOverMap.put(goods.getId(), false);
}
redisService.delete(OrderKey.getMiaoshaOrderByUidGid);
redisService.delete(MiaoshaKey.isGoodsOver);
miaoshaService.reset(goodsList);
return Result.success(true);
}
@RequestMapping(value="/{path}/do_miaosha", method=RequestMethod.POST)
@ResponseBody
public Result<Integer> miaosha(Model model,MiaoshaUser user,
@RequestParam("goodsId")long goodsId,
@PathVariable("path") String path) {
model.addAttribute("user", user);
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//驗證path
boolean check = miaoshaService.checkPath(user, goodsId, path);
if(!check){
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
//記憶體標記,減少redis訪問
boolean over = localOverMap.get(goodsId);
if(over) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//預減庫存
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10
if(stock < 0) {
localOverMap.put(goodsId, true);
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判斷是否已經秒殺到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//入隊
MiaoshaMessage mm = new MiaoshaMessage();
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);
return Result.success(0);//排隊中
}
/**
* orderId:成功
* -1:秒殺失敗
* 0: 排隊中
* */
@RequestMapping(value="/result", method=RequestMethod.GET)
@ResponseBody
public Result<Long> miaoshaResult(Model model,MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
long result =miaoshaService.getMiaoshaResult(user.getId(), goodsId);
return Result.success(result);
}
@AccessLimit(seconds=5, maxCount=5, needLogin=true)
@RequestMapping(value="/path", method=RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
@RequestParam("goodsId")long goodsId,
@RequestParam(value="verifyCode", defaultValue="0")int verifyCode
) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
if(!check) {
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
String path =miaoshaService.createMiaoshaPath(user, goodsId);
return Result.success(path);
}
@RequestMapping(value="/verifyCode", method=RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaVerifyCod(HttpServletResponse response,MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
try {
BufferedImage image = miaoshaService.createVerifyCode(user, goodsId);
OutputStream out = response.getOutputStream();
ImageIO.write(image, "JPEG", out);
out.flush();
out.close();
return null;
}catch(Exception e) {
e.printStackTrace();
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
}
}
前端主要程式碼:
<div class="row">
<div class="form-inline">
<img id="verifyCodeImg" width="80" height="32" style="display:none" onclick="refreshVerifyCode()"/>
<input id="verifyCode" class="form-control" style="display:none"/>
<button class="btn btn-primary" type="button" id="buyButton"onclick="getMiaoshaPath()">立即秒殺</button>
</div>
</div>
function getMiaoshaPath(){
var goodsId = $("#goodsId").val();
g_showLoading();
$.ajax({
url:"/miaosha/path",
type:"GET",
data:{
goodsId:goodsId,
verifyCode:$("#verifyCode").val()
},
success:function(data){
if(data.code == 0){
var path = data.data;
doMiaosha(path);
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客戶端請求有誤");
}
});
function getMiaoshaResult(goodsId){
g_showLoading();
$.ajax({
url:"/miaosha/result",
type:"GET",
data:{
goodsId:$("#goodsId").val(),
},
success:function(data){
if(data.code == 0){
var result = data.data;
if(result < 0){
layer.msg("對不起,秒殺失敗");
}else if(result == 0){//繼續輪詢
setTimeout(function(){
getMiaoshaResult(goodsId);
}, 200);
}else{
layer.confirm("恭喜你,秒殺成功!檢視訂單?", {btn:["確定","取消"]},
function(){
window.location.href="/order_detail.htm?orderId="+result;
},
function(){
layer.closeAll();
});
}
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客戶端請求有誤");
}
});
}
function doMiaosha(path){
$.ajax({
url:"/miaosha/"+path+"/do_miaosha",
type:"POST",
data:{
goodsId:$("#goodsId").val()
},
success:function(data){
if(data.code == 0){
//window.location.href="/order_detail.htm?orderId="+data.data.id;
getMiaoshaResult($("#goodsId").val());
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客戶端請求有誤");
}
});
}
function refreshVerifyCode(){
$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val()+"×tamp="+new Date().getTime());
}
附錄一:資料庫的設計
商品表:
create table Goods (
id bigint PRIMARY KEY ,
goodsName VARCHAR(255) ,
goodsTitle VARCHAR(255) ,
goodsImg VARCHAR(255) ,
goodsDetail VARCHAR(255) ,
goodsPrice double precision not null ,
goodsStock INTEGER
)ENGINE =INNODB DEFAULT CHARSET= utf8;
秒殺商品表:
create table MiaoshaGoods (
id bigint PRIMARY KEY ,
goodsId bigint ,
stockCount INTEGER ,
startDate null ,
endDate null
)ENGINE =INNODB DEFAULT CHARSET= utf8;
秒殺訂單表:
create table MiaoshaOrder (
id bigint PRIMARY KEY ,
userId bigint ,
orderId bigint ,
goodsId bigint
)ENGINE =INNODB DEFAULT CHARSET= utf8;
使用者表:
create table MiaoshaUser (
id bigint PRIMARY KEY ,
nickname VARCHAR(255) ,
password VARCHAR(255) ,
salt VARCHAR(255) ,
head VARCHAR(255) ,
registerDate null ,
lastLoginDate null ,
loginCount INTEGER
)ENGINE =INNODB DEFAULT CHARSET= utf8;
訂單表:
create table OrderInfo (
id bigint PRIMARY KEY ,
userId bigint ,
goodsId bigint ,
deliveryAddrId bigint ,
goodsName VARCHAR(255) ,
goodsCount INTEGER ,
goodsPrice double precision not null ,
orderChannel INTEGER ,
status INTEGER ,
createDate null ,
payDate null
)ENGINE =INNODB DEFAULT CHARSET= utf8;
附錄二:攔截器內實現的介面限流
@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {
int seconds();
int maxCount();
boolean needLogin() default true;
}
@Service
public class AccessInterceptor extends HandlerInterceptorAdapter{
@Autowired
MiaoshaUserService userService;
@Autowired
RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(handler instanceof HandlerMethod) {
MiaoshaUser user = getUser(request, response);
UserContext.setUser(user);
HandlerMethod hm = (HandlerMethod)handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit == null) {
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if(needLogin) {
if(user == null) {
render(response, CodeMsg.SESSION_ERROR);
return false;
}
key += "_" + user.getId();
}else {
//do nothing
}
AccessKey ak = AccessKey.withExpire(seconds);
Integer count = redisService.get(ak, key, Integer.class);
if(count == null) {
redisService.set(ak, key, 1);
}else if(count < maxCount) {
redisService.incr(ak, key);
}else {
render(response, CodeMsg.ACCESS_LIMIT_REACHED);
return false;
}
}
return true;
}
private void render(HttpServletResponse response, CodeMsg cm)throws Exception {
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(Result.error(cm));
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookiName) {
Cookie[] cookies = request.getCookies();
if(cookies == null || cookies.length <= 0){
return null;
}
for(Cookie cookie : cookies) {
if(cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}
}
參考程式碼
https://gitee.com/lzhcode/maven-parent/tree/master/lzh-seckill2
相關文章
- 如何設計一個秒殺系統?
- 如何設計一個優秀的秒殺系統?
- 怎麼設計一個秒殺系統
- 【讀書筆記】如何設計一個秒殺系統筆記
- 如何設計一個高可用、高併發秒殺系統
- 秒殺系統設計
- [系統設計]秒殺系統
- 經驗:一個秒殺系統的設計思考
- 面試必考:秒殺系統如何設計?面試
- 如何設計一個秒殺系統-許令波-極客時間
- 秒殺系統的設計
- 秒殺系統:如何打造並維護一個超大流量的秒殺系統?
- 秒殺系統設計的5個要點
- 電商秒殺系統設計
- 秒殺系統架構如何設計之我見架構
- 《吊打面試官》系列-秒殺系統設計面試
- 如何設計電商行業億級使用者秒殺系統行業
- 秒殺系統
- 阿里雙11秒殺如何設計?阿里
- 秒殺系統設計中的業務性思考
- 基於雲原生的秒殺系統設計思路
- 阿里的秒殺系統是怎麼設計的?阿里
- 秒殺系統分析
- Redis秒殺系統架構設計-微信搶紅包Redis架構
- 單機秒殺系統的架構設計與實現架構
- 如何設計一個RPC系統?RPC
- 如何設計一個 RPC 系統RPC
- 高併發秒殺系統架構詳解,不是所有的秒殺都是秒殺!架構
- 你設計的秒殺系統,能通過 “雙十一” 大考嗎?
- 秒殺架構模型設計架構模型
- 【高併發】秒殺系統架構解密,不是所有的秒殺都是秒殺(升級版)!!架構解密
- 如何設計一個微博系統?- 4招教你搞定系統設計
- 如何設計一個搶紅包系統
- 商城秒殺系統總結(Java)Java
- 微服務電商秒殺系統微服務
- 分散式抽獎秒殺系統,DDD架構設計和實現分享分散式架構
- Redis輕鬆實現秒殺系統Redis
- 架構 秒殺系統優化思路架構優化