java秒杀项目总结
java秒殺項目總結(jié)
本項目專攻秒殺模塊,共分為七個章節(jié)
第一章 項目框架搭建
1.Spring Boot環(huán)境搭建
 2.集成Thymeleaf , Result結(jié)果封裝
- 前期前后端并未分離,使用Thymeleaf來獲取后臺傳來的數(shù)據(jù)
 - Result結(jié)果封裝可以讓代碼更規(guī)范,成功的時候只傳數(shù)據(jù),失敗的時候傳遞狀態(tài)碼
 
3.集成Mybatis+ Druid
 4.集成Jedis+ Redis安裝+通用緩存Key封裝
- 這里使用的是自己封裝的jedis
 - 通用緩存key封裝,定義一個接口,過期時間和緩存前綴。抽象類繼承接口實現(xiàn)通用緩存名字和過期時間,再有各種key繼承抽象類,實現(xiàn)通用緩存
 
第二章實現(xiàn)登錄功能
1.數(shù)據(jù)庫設(shè)計
2.明文密碼兩次MD5處理
 兩次加密:
-  
1、當(dāng)你輸入提交到表單使用md5對輸入的密碼加密
 -  
2、當(dāng)你將表單中的密碼插入到數(shù)據(jù)庫時,再對表單的密碼加密
 -  
為什么兩次md5?
客戶端:我們使用密碼+固定Salt來形成最終密碼
服務(wù)端:將用戶輸入 
3 JSR303參數(shù)檢驗+全局異常處理器
 為什么要做JSR303參數(shù)檢驗?
 前端的校驗只是有效性的校驗(手機號輸錯,密碼錯誤),服務(wù)端的校驗是防止惡意的用戶。
 JSR303檢驗賬號是否符合規(guī)范標(biāo)準(zhǔn),@IsMobile自己寫的注解
@Valid注解寫在輸入?yún)?shù)前面,輸入?yún)?shù)對應(yīng)的類,里面的各項成員變量上面還能加注解約束
@RequestMapping("/do_login")@ResponseBodypublic Result<String> doLogin(HttpServletResponse response,@Valid LoginVo loginVo){log.info(loginVo.toString());//登錄String token=miaoshaUserService.login(response,loginVo);return Result.success(token);}當(dāng)參數(shù)校驗返回false即校驗失敗時,那么就會出現(xiàn)一個BindException異常,為了顯示友好就寫一個全局異常處理器去攔截這個異常。當(dāng)然其他的異常也能夠被攔截。
怎么實現(xiàn)友好顯示的?
 當(dāng)用戶登錄時,如果后臺登錄方法查不到用戶或者密碼不匹配那么就會拋一個全局異常,拋出的這個異常會被我們定義的全局異常處理器攔截,攔截到之后會return一個錯誤信息,前臺ajax就會回調(diào)顯示這個錯誤信息,用戶能更友好的看到錯誤信息。
4.分布式Session
 背景:分布式集群,多臺服務(wù)器??蛻舳说谝淮握埱舐湓诘谝慌_服務(wù)器上,第二次請求落在第二臺服務(wù)器上。那么第二次Session就會丟失。
解決方案: 1.容器原生的Session同步,就是將一臺計算機上的Session同步到其他計算機上,這樣性能開銷大。
2.分布式Session,實際情況中用的比較多。Session并沒有存到容器中來而是存到了緩存中,這就是分布式Session。
分布式Session具體實現(xiàn): 用戶登錄成功,會生成一個token。token用于生成鍵,用戶信息作為值,將這對鍵值對存到redis中,然后實例一個Cookie(“token”,token),將這個Cookie寫進去寫到response中,那么下次這個用戶再次發(fā)請求就會帶著這個Cookie。配置參數(shù)解析器就能根據(jù)Cookie攜帶的值到redis中查到用戶信息,然后注入到方法的請求參數(shù)中。
public String login(HttpServletResponse response, LoginVo loginVo) {。。。。//生成cookieString token= UUIDUtil.uuid();//cookie寫到response,session寫到redisaddCookie(response,token,user);return token;} private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {redisService.set(MiaoshaUserKey.token,token,user);Cookie cookie=new Cookie(COOKI_NAME_TOKEN,token);cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());cookie.setPath("/");response.addCookie(cookie);}第三章實現(xiàn)秒殺功能
1.數(shù)據(jù)庫設(shè)計
 數(shù)據(jù)庫并沒有遵循三范式,有冗余,但是冗余是必須的。
 2.商品列表頁
 3.商品詳情頁
 4.訂單詳情頁
秒殺功能:
 三步:
 判斷庫存、判斷是否已經(jīng)秒殺到了、減庫存下訂單(事務(wù))。
 賣超:
 (1)減庫存SQL,加上庫存是否小于零的條件。
 (2)訂單表結(jié)構(gòu)增加唯一索引(用戶id和秒殺商品id),防止一個用戶下多次單。
 (3)減庫存這個操作的返回值為1的時候才繼續(xù)后面的下訂單,否則會出現(xiàn)生成的訂單數(shù)量遠遠多于賣出商品的數(shù)量。
另外還實現(xiàn)了倒計時功能,判斷當(dāng)前是否可以秒殺(就是比較時間的大小):
@RequestMapping("/to_detail/{goodsId}") public String detail(Model model,MiaoshaUser user,@PathVariable("goodsId")long goodsId) {model.addAttribute("user", user);GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);model.addAttribute("goods", goods);long startAt = goods.getStartDate().getTime();long endAt = goods.getEndDate().getTime();long now = System.currentTimeMillis();int miaoshaStatus = 0;int remainSeconds = 0;if(now < startAt ) {//秒殺還沒開始,倒計時miaoshaStatus = 0;remainSeconds = (int)((startAt - now )/1000);}else if(now > endAt){//秒殺已經(jīng)結(jié)束miaoshaStatus = 2;remainSeconds = -1;}else {//秒殺進行中miaoshaStatus = 1;remainSeconds = 0;}model.addAttribute("miaoshaStatus", miaoshaStatus);model.addAttribute("remainSeconds", remainSeconds);return "goods_detail"; } function countDown(){var remainSeconds = $("#remainSeconds").val();var timeout;if(remainSeconds > 0){//秒殺還沒開始,倒計時$("#buyButton").attr("disabled", true);timeout = setTimeout(function(){$("#countDown").text(remainSeconds - 1);$("#remainSeconds").val(remainSeconds - 1);countDown();},1000);}else if(remainSeconds == 0){//秒殺進行中$("#buyButton").attr("disabled", false);if(timeout){clearTimeout(timeout);}$("#miaoshaTip").html("秒殺進行中");}else{//秒殺已經(jīng)結(jié)束$("#buyButton").attr("disabled", true);$("#miaoshaTip").html("秒殺已經(jīng)結(jié)束");} }第四章JMeter壓測
1, JMeter入門
 2,自定義變量模擬多用戶
 生成500個用戶的token和密碼保存到一個文件當(dāng)中,壓測時加載文件模擬多用戶
第五章頁面優(yōu)化技術(shù)
大并發(fā)的瓶頸就是數(shù)據(jù)庫。應(yīng)對并發(fā)最有效的就是緩存
1.頁面緩存+ URL緩存+對象緩存
頁面緩存適用場景:適合于變化不大的場景,比如商品列表。實際項目中商品列表可能會分頁,不可能每頁都緩存,只是緩存前兩頁。
頁面緩存:第一次請求過來就將渲染好的頁面存到redis中,下次請求就直接從redis中取頁面。
頁面緩存并不是將所有頁面都緩存,而是將變化不大的,頁面緩存和URL緩存都設(shè)置過期時間(60s),而對象緩存根據(jù)token獲取用戶,且對象緩存永久有效,
@RequestMapping(value = "/to_list",produces = "text/html")@ResponseBodypublic String list(HttpServletRequest request, HttpServletResponse response,Model model, MiaoshaUser user) {model.addAttribute("user", user);//1、先取緩存String html = redisService.get(GoodsKey.getGoodsList, "", String.class);if(!StringUtils.isEmpty(html)){//緩存不為空return html;}List<GoodsVo> goodsList = goodsService.listGoodsVo();model.addAttribute("goodsList",goodsList);//return "goods_list";//緩存為空IWebContext ctx=new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());//手動渲染html=thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);if(!StringUtils.isEmpty(html)){redisService.set(GoodsKey.getGoodsList,"",html); //存入緩存}return html;}對象緩存:實現(xiàn)分布式Session就是,將用戶對象緩存到redis中。
2.頁面靜態(tài)化,前后端分離
頁面靜態(tài)化:就是瀏覽器將HTML頁面存在客戶端,通過ajax獲取數(shù)據(jù)拿到客戶端在渲染頁面。(這樣就不用下載頁面了,只需要下載動態(tài)數(shù)據(jù)就好了。
//商品列表頁跳轉(zhuǎn)到商品詳情頁面,goods_detail.htm放到靜態(tài)文件夾里面 <td><a th:href="'/goods_detail.htm?goodsId='+${goods.id}">詳情</a></td>//靜態(tài)頁面goods_detail.htm,里面的js $(function(){//countDown();getDetail(); });function getDetail(){//這個方法獲取請求傳過來的參數(shù)var goodsId = g_getQueryString("goodsId");$.ajax({url:"/goods/detail/"+goodsId,type:"GET",success:function(data){if(data.code == 0){//渲染頁面的方法render(data.data);}else{layer.msg(data.msg);}},error:function(){layer.msg("客戶端請求有誤");}}); }3.靜態(tài)資源優(yōu)化
注意:js文件在瀏覽器本地會有緩存,如果改動了js文件,下次請求加載的還是本地緩存的js文件,導(dǎo)致前端代碼跑不通。解決方法引入js文件的鏈接后面加一個版本參數(shù)。代碼跑不通就debug,查看數(shù)據(jù)流是不是對的,這樣能盡快鎖定哪里出了問題。
第六章接口優(yōu)化
總目標(biāo):減少數(shù)據(jù)庫的訪問量。
如何對他做優(yōu)化?
減少對數(shù)據(jù)庫的訪問, redis和mq
把訂單同步下單改為異步
好處:庫存不足后,后面的請求對數(shù)據(jù)庫基本沒有壓力
異步下單,既不是返回成功,也不是返回失敗,而是返回排隊中
1 Redis預(yù)減庫存減少數(shù)據(jù)庫訪問
容器初始化的時候?qū)⒚霘⑸唐返膸齑婧蛢?nèi)存標(biāo)記加載到Redis中,前面來的請求將redis緩存的庫存減完后,后面的請求過來直接返回秒殺結(jié)束。
2.內(nèi)存標(biāo)記減少Redis訪問
比如說前面10個請求已經(jīng)將redis中緩存的庫存減到0了,那么后面的請求會繼續(xù)將redis中的庫存減為負數(shù),顯然后面的請求將redis中的庫存減成負數(shù)是多余的,而且還增加了redis的訪問量。那么這里就做一個內(nèi)存標(biāo)記,緩存中庫存大于零的時候內(nèi)存標(biāo)記為false,當(dāng)緩存中的庫存減為0時內(nèi)存標(biāo)記就為true。當(dāng)為false時請求能往下走,反之直接返回秒殺結(jié)束。
3. RabbitMQ隊列緩沖,異步下單,增強用戶體驗
服務(wù)端異步的請求出隊,將訂單寫到緩存,用戶去查找,看成功還是失敗
創(chuàng)建秒殺信息類
MiaoshaMessage(用戶信息和秒殺商品id)
將信息類發(fā)送出去
Direct交換機,將信息類對象轉(zhuǎn)為字符串,進隊
接收者:將string還原為對象
從信息類里面拿用戶信息和商品id后
入隊成功的時候去輪詢
怎么做輪詢?判斷一個用戶有沒有秒殺到商品
獲取秒殺結(jié)果,調(diào)用方法(如果秒殺訂單不為空,成功,等于空,兩種情況,失敗和排隊中,無法辨別,這是根據(jù)標(biāo)記來判斷是不是因為庫存不足導(dǎo)致的失敗)
生成庫存不足標(biāo)記的方法,往redis里面設(shè)置一個值,新建miaoshakey,永久生效
如果redis里面存在這個key,就說明賣完了
4. RabbitMQ安裝與Spring Boot集成
package com.imooc.miaosha.controller;import com.imooc.miaosha.access.AccessLimit; import com.imooc.miaosha.domain.MiaoshaMessage; import com.imooc.miaosha.rabbitmq.MQSender; import com.imooc.miaosha.redis.*; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*;import com.imooc.miaosha.domain.MiaoshaOrder; import com.imooc.miaosha.domain.MiaoshaUser; import com.imooc.miaosha.domain.OrderInfo; import com.imooc.miaosha.result.CodeMsg; import com.imooc.miaosha.result.Result; import com.imooc.miaosha.service.GoodsService; import com.imooc.miaosha.service.MiaoshaService; import com.imooc.miaosha.service.MiaoshaUserService; import com.imooc.miaosha.service.OrderService; import com.imooc.miaosha.vo.GoodsVo;import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.HashMap; import java.util.List;@Controller @RequestMapping("/miaosha") public class MiaoshaController implements InitializingBean {@AutowiredMiaoshaUserService userService;@AutowiredRedisService redisService;@AutowiredGoodsService goodsService;@AutowiredOrderService orderService;@AutowiredMiaoshaService miaoshaService;@AutowiredMQSender sender;private HashMap<Long,Boolean> localOverMap=new HashMap<Long,Boolean>();//系統(tǒng)初始化時,將庫存加載進緩存,并將秒殺商品的狀態(tài)標(biāo)記為false@Overridepublic void afterPropertiesSet() throws Exception {List<GoodsVo> goodsList = goodsService.listGoodsVo();//判斷一下商品列表是否為空if(goodsList==null){return;}for (GoodsVo goods : goodsList) {Integer stockCount = goods.getStockCount();redisService.set(GoodsKey.getMiaoshaGoodsStock,""+goods.getId(),stockCount);localOverMap.put(goods.getId(),false);}}/*** QPS:* 1000 * 10* */@RequestMapping(value="/{path}/do_miaosha", method=RequestMethod.POST)@ResponseBodypublic 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);}//校驗秒殺pathboolean check=miaoshaService.checkPath(user,goodsId,path);if(!check){return Result.error(CodeMsg.REQUEST_ERROR);}//先判斷一下該秒殺商品的狀態(tài)(內(nèi)存標(biāo)記,減少redis訪問)Boolean b = localOverMap.get(goodsId);if(b){ //說明緩存中的商品已經(jīng)減為0return Result.error(CodeMsg.MIAO_SHA_OVER);}//收到請求,減少緩存中的庫存long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);if(stock<0){localOverMap.put(goodsId,true);return Result.error(CodeMsg.MIAO_SHA_OVER);}//判斷是否已經(jīng)秒殺到了MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);if(order != null) {return Result.error(CodeMsg.REPEATE_MIAOSHA);}//入隊MiaoshaMessage miaoshaMessage=new MiaoshaMessage();miaoshaMessage.setUser(user);miaoshaMessage.setGoodsId(goodsId);sender.sendMiaoshaMessage(miaoshaMessage);return Result.success(0);//0代表排隊中/*//判斷庫存GoodsVo goods = goodsService.getGoodsVoById(goodsId);//10個商品,req1 req2int stock = goods.getStockCount();if(stock <= 0) {return Result.error(CodeMsg.MIAO_SHA_OVER);}//判斷是否已經(jīng)秒殺到了MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);if(order != null) {return Result.error(CodeMsg.REPEATE_MIAOSHA);}//減庫存 下訂單 寫入秒殺訂單OrderInfo orderInfo = miaoshaService.miaosha(user, goods);return Result.success(orderInfo);*/}/** 返回orderId :成功* -1:秒殺失敗* 0:排隊中* */@RequestMapping(value="/result", method=RequestMethod.GET)@ResponseBodypublic Result<Long> miaoshaResult(Model model,MiaoshaUser user,@RequestParam("goodsId")long goodsId) {model.addAttribute("user", user);if (user == null) {return Result.error(CodeMsg.SESSION_ERROR);}//獲取秒殺結(jié)果long result=miaoshaService.getMiaoshaResult(user.getId(), goodsId);return Result.success(result);}} }缺陷:庫存在緩存中的key是永不過期的,當(dāng)你該庫存的時候,需要將緩存中的key先刪除
/*** orderId:成功* -1:秒殺失敗* 0: 排隊中* */public long getMiaoshaResult(Long userId, long goodsId) {MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);if(order==null){//如果訂單為空,有兩種狀態(tài),排隊和庫存不足導(dǎo)致的失敗,根據(jù)標(biāo)記狀態(tài)來判斷boolean isOver = getGoodsOver(goodsId);if(isOver){return -1;}else{return 0;}}else {return order.getOrderId();}}在做減庫存操作時,如果減庫存失敗,在緩存中添加一個key。因此在去查詢秒殺結(jié)果時,如果訂單為空(有兩種狀態(tài),排隊和庫存不足導(dǎo)致的失敗)再根據(jù)標(biāo)記狀態(tài)(緩存中有沒有對應(yīng)的key)來判斷是那種個情況,如果有說明是庫存不足,如果沒有說明是正在排隊中。
5.訪問Nginx水平擴展
 系統(tǒng)的負載均衡nginx,如果前面沒有加緩存,單群加服務(wù)器沒有作用,全都落在db上,db并發(fā)是有限的,再加服務(wù)器也是沒用的,我們基于帶有有良好的擴展性。
第七章安全優(yōu)化
防止惡意用戶刷我們的接口,秒殺開始之前不知道訪問那個地址,比較安全
驗證碼作用: 1、防止機器人或工具刷
 2、沒有驗證碼,大家只是點擊鼠標(biāo)請求集中,數(shù)據(jù)庫壓力大 (有的話消耗時間,將瞬間的并發(fā)量分散到10s開)
接口限流防刷: 系統(tǒng)本身容量有限,防止用戶惡意刷接口,在某個時間端內(nèi)限制用戶訪問的次數(shù)。
1.秒殺接口地址隱藏
思路:秒殺開始之前,先去請求接口獲取秒殺地址
1.接口改造,帶上PathVariable參數(shù)
 2.添加生成地址的接口
 3.秒殺收到請求,先驗證PathVariable
前端拿到path后在調(diào)用秒殺接口
 秒殺要接受path,校驗
 怎么驗證? get緩存redis里面的key和傳過來的path是否相等
2.數(shù)學(xué)公式驗證碼
public boolean checkverifyCode(MiaoshaUser user, long goodsId, int verifyCode) {if(user == null || goodsId <=0) {return false;}//從緩存中取驗證碼和輸入的比較Integer OldCode = redisService.get(MiaoshaKey.getMiaoshaVerifyCode, user.getId() + "," + goodsId, Integer.class);if(OldCode==null || OldCode-verifyCode!=0){return false;}//驗證之后,將緩存中的驗證碼刪除redisService.delete(MiaoshaKey.getMiaoshaVerifyCode,user.getId() + "," + goodsId);return true;}3.接口防刷
需求:設(shè)置10秒鐘內(nèi),最多請求5次,超過這個次數(shù)就算為非法請求,提示訪問太頻繁。
 設(shè)計:使用攔截器,將這個功能與業(yè)務(wù)代碼分離,能讓其他方法形成復(fù)用。
獲取注解上的時間,設(shè)置為緩存key的過期時間。去緩存中獲取已訪問次數(shù),如果緩存為空的話,說明第一次訪問,設(shè)置緩存并將次數(shù)設(shè)為1。之后在不超過最大訪問次數(shù)的基礎(chǔ)上,每次訪問緩存中的數(shù)加1.
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if(handler instanceof HandlerMethod){//先獲取用戶MiaoshaUser user=getUser(request,response);//將獲取的用戶存起來,方便后面的調(diào)用傳遞UserContext.setUser(user);HandlerMethod hm=(HandlerMethod)handler;//獲取方法上的注解AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);if (accessLimit==null){return true; //沒有注解}//有注解獲取注解的參數(shù)int seconds=accessLimit.seconds();int maxCount=accessLimit.maxCount();boolean needLogin=accessLimit.needLogin();//獲取keyString key=request.getRequestURI();//如果需要登陸if(needLogin){if(user==null){//提示錯誤信息render(response,CodeMsg.SESSION_ERROR);return false;}//key需要加上用戶idkey+="_"+user.getId();}else {//如果不需要登陸什么都不做}//查詢訪問次數(shù)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 {//大于次數(shù)render(response,CodeMsg.ACCESS_ERROR);return false;}}return true; }自定義的注解
@Retention(RUNTIME) @Target(METHOD) public @interface AccessLimit {int seconds();int maxCount();boolean needLogin() default true;}總結(jié)
以上是生活随笔為你收集整理的java秒杀项目总结的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 关于java中BufferedReade
 - 下一篇: idea中连接mysql插入成功数据 在