java秒杀项目总结
java秒殺項(xiàng)目總結(jié)
本項(xiàng)目專攻秒殺模塊,共分為七個(gè)章節(jié)
第一章 項(xiàng)目框架搭建
1.Spring Boot環(huán)境搭建
2.集成Thymeleaf , Result結(jié)果封裝
- 前期前后端并未分離,使用Thymeleaf來獲取后臺(tái)傳來的數(shù)據(jù)
- Result結(jié)果封裝可以讓代碼更規(guī)范,成功的時(shí)候只傳數(shù)據(jù),失敗的時(shí)候傳遞狀態(tài)碼
3.集成Mybatis+ Druid
4.集成Jedis+ Redis安裝+通用緩存Key封裝
- 這里使用的是自己封裝的jedis
- 通用緩存key封裝,定義一個(gè)接口,過期時(shí)間和緩存前綴。抽象類繼承接口實(shí)現(xiàn)通用緩存名字和過期時(shí)間,再有各種key繼承抽象類,實(shí)現(xiàn)通用緩存
第二章實(shí)現(xiàn)登錄功能
1.數(shù)據(jù)庫設(shè)計(jì)
2.明文密碼兩次MD5處理
兩次加密:
-
1、當(dāng)你輸入提交到表單使用md5對輸入的密碼加密
-
2、當(dāng)你將表單中的密碼插入到數(shù)據(jù)庫時(shí),再對表單的密碼加密
-
為什么兩次md5?
客戶端:我們使用密碼+固定Salt來形成最終密碼
服務(wù)端:將用戶輸入
3 JSR303參數(shù)檢驗(yàn)+全局異常處理器
為什么要做JSR303參數(shù)檢驗(yàn)?
前端的校驗(yàn)只是有效性的校驗(yàn)(手機(jī)號(hào)輸錯(cuò),密碼錯(cuò)誤),服務(wù)端的校驗(yàn)是防止惡意的用戶。
JSR303檢驗(yàn)賬號(hào)是否符合規(guī)范標(biāo)準(zhǔn),@IsMobile自己寫的注解
@Valid注解寫在輸入?yún)?shù)前面,輸入?yún)?shù)對應(yīng)的類,里面的各項(xià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ù)校驗(yàn)返回false即校驗(yàn)失敗時(shí),那么就會(huì)出現(xiàn)一個(gè)BindException異常,為了顯示友好就寫一個(gè)全局異常處理器去攔截這個(gè)異常。當(dāng)然其他的異常也能夠被攔截。
怎么實(shí)現(xiàn)友好顯示的?
當(dāng)用戶登錄時(shí),如果后臺(tái)登錄方法查不到用戶或者密碼不匹配那么就會(huì)拋一個(gè)全局異常,拋出的這個(gè)異常會(huì)被我們定義的全局異常處理器攔截,攔截到之后會(huì)return一個(gè)錯(cuò)誤信息,前臺(tái)ajax就會(huì)回調(diào)顯示這個(gè)錯(cuò)誤信息,用戶能更友好的看到錯(cuò)誤信息。
4.分布式Session
背景:分布式集群,多臺(tái)服務(wù)器。客戶端第一次請求落在第一臺(tái)服務(wù)器上,第二次請求落在第二臺(tái)服務(wù)器上。那么第二次Session就會(huì)丟失。
解決方案: 1.容器原生的Session同步,就是將一臺(tái)計(jì)算機(jī)上的Session同步到其他計(jì)算機(jī)上,這樣性能開銷大。
2.分布式Session,實(shí)際情況中用的比較多。Session并沒有存到容器中來而是存到了緩存中,這就是分布式Session。
分布式Session具體實(shí)現(xiàn): 用戶登錄成功,會(huì)生成一個(gè)token。token用于生成鍵,用戶信息作為值,將這對鍵值對存到redis中,然后實(shí)例一個(gè)Cookie(“token”,token),將這個(gè)Cookie寫進(jìn)去寫到response中,那么下次這個(gè)用戶再次發(fā)請求就會(huì)帶著這個(gè)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);}第三章實(shí)現(xiàn)秒殺功能
1.數(shù)據(jù)庫設(shè)計(jì)
數(shù)據(jù)庫并沒有遵循三范式,有冗余,但是冗余是必須的。
2.商品列表頁
3.商品詳情頁
4.訂單詳情頁
秒殺功能:
三步:
判斷庫存、判斷是否已經(jīng)秒殺到了、減庫存下訂單(事務(wù))。
賣超:
(1)減庫存SQL,加上庫存是否小于零的條件。
(2)訂單表結(jié)構(gòu)增加唯一索引(用戶id和秒殺商品id),防止一個(gè)用戶下多次單。
(3)減庫存這個(gè)操作的返回值為1的時(shí)候才繼續(xù)后面的下訂單,否則會(huì)出現(xiàn)生成的訂單數(shù)量遠(yuǎn)遠(yuǎn)多于賣出商品的數(shù)量。
另外還實(shí)現(xiàn)了倒計(jì)時(shí)功能,判斷當(dāng)前是否可以秒殺(就是比較時(shí)間的大小):
@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 ) {//秒殺還沒開始,倒計(jì)時(shí)miaoshaStatus = 0;remainSeconds = (int)((startAt - now )/1000);}else if(now > endAt){//秒殺已經(jīng)結(jié)束miaoshaStatus = 2;remainSeconds = -1;}else {//秒殺進(jìn)行中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){//秒殺還沒開始,倒計(jì)時(shí)$("#buyButton").attr("disabled", true);timeout = setTimeout(function(){$("#countDown").text(remainSeconds - 1);$("#remainSeconds").val(remainSeconds - 1);countDown();},1000);}else if(remainSeconds == 0){//秒殺進(jìn)行中$("#buyButton").attr("disabled", false);if(timeout){clearTimeout(timeout);}$("#miaoshaTip").html("秒殺進(jìn)行中");}else{//秒殺已經(jīng)結(jié)束$("#buyButton").attr("disabled", true);$("#miaoshaTip").html("秒殺已經(jīng)結(jié)束");} }第四章JMeter壓測
1, JMeter入門
2,自定義變量模擬多用戶
生成500個(gè)用戶的token和密碼保存到一個(gè)文件當(dāng)中,壓測時(shí)加載文件模擬多用戶
第五章頁面優(yōu)化技術(shù)
大并發(fā)的瓶頸就是數(shù)據(jù)庫。應(yīng)對并發(fā)最有效的就是緩存
1.頁面緩存+ URL緩存+對象緩存
頁面緩存適用場景:適合于變化不大的場景,比如商品列表。實(shí)際項(xiàng)目中商品列表可能會(huì)分頁,不可能每頁都緩存,只是緩存前兩頁。
頁面緩存:第一次請求過來就將渲染好的頁面存到redis中,下次請求就直接從redis中取頁面。
頁面緩存并不是將所有頁面都緩存,而是將變化不大的,頁面緩存和URL緩存都設(shè)置過期時(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());//手動(dòng)渲染html=thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);if(!StringUtils.isEmpty(html)){redisService.set(GoodsKey.getGoodsList,"",html); //存入緩存}return html;}對象緩存:實(shí)現(xiàn)分布式Session就是,將用戶對象緩存到redis中。
2.頁面靜態(tài)化,前后端分離
頁面靜態(tài)化:就是瀏覽器將HTML頁面存在客戶端,通過ajax獲取數(shù)據(jù)拿到客戶端在渲染頁面。(這樣就不用下載頁面了,只需要下載動(dòng)態(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(){//這個(gè)方法獲取請求傳過來的參數(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文件在瀏覽器本地會(huì)有緩存,如果改動(dòng)了js文件,下次請求加載的還是本地緩存的js文件,導(dǎo)致前端代碼跑不通。解決方法引入js文件的鏈接后面加一個(gè)版本參數(shù)。代碼跑不通就debug,查看數(shù)據(jù)流是不是對的,這樣能盡快鎖定哪里出了問題。
第六章接口優(yōu)化
總目標(biāo):減少數(shù)據(jù)庫的訪問量。
如何對他做優(yōu)化?
減少對數(shù)據(jù)庫的訪問, redis和mq
把訂單同步下單改為異步
好處:庫存不足后,后面的請求對數(shù)據(jù)庫基本沒有壓力
異步下單,既不是返回成功,也不是返回失敗,而是返回排隊(duì)中
1 Redis預(yù)減庫存減少數(shù)據(jù)庫訪問
容器初始化的時(shí)候?qū)⒚霘⑸唐返膸齑婧蛢?nèi)存標(biāo)記加載到Redis中,前面來的請求將redis緩存的庫存減完后,后面的請求過來直接返回秒殺結(jié)束。
2.內(nèi)存標(biāo)記減少Redis訪問
比如說前面10個(gè)請求已經(jīng)將redis中緩存的庫存減到0了,那么后面的請求會(huì)繼續(xù)將redis中的庫存減為負(fù)數(shù),顯然后面的請求將redis中的庫存減成負(fù)數(shù)是多余的,而且還增加了redis的訪問量。那么這里就做一個(gè)內(nèi)存標(biāo)記,緩存中庫存大于零的時(shí)候內(nèi)存標(biāo)記為false,當(dāng)緩存中的庫存減為0時(shí)內(nèi)存標(biāo)記就為true。當(dāng)為false時(shí)請求能往下走,反之直接返回秒殺結(jié)束。
3. RabbitMQ隊(duì)列緩沖,異步下單,增強(qiáng)用戶體驗(yàn)
服務(wù)端異步的請求出隊(duì),將訂單寫到緩存,用戶去查找,看成功還是失敗
創(chuàng)建秒殺信息類
MiaoshaMessage(用戶信息和秒殺商品id)
將信息類發(fā)送出去
Direct交換機(jī),將信息類對象轉(zhuǎn)為字符串,進(jìn)隊(duì)
接收者:將string還原為對象
從信息類里面拿用戶信息和商品id后
入隊(duì)成功的時(shí)候去輪詢
怎么做輪詢?判斷一個(gè)用戶有沒有秒殺到商品
獲取秒殺結(jié)果,調(diào)用方法(如果秒殺訂單不為空,成功,等于空,兩種情況,失敗和排隊(duì)中,無法辨別,這是根據(jù)標(biāo)記來判斷是不是因?yàn)閹齑娌蛔銓?dǎo)致的失敗)
生成庫存不足標(biāo)記的方法,往redis里面設(shè)置一個(gè)值,新建miaoshakey,永久生效
如果redis里面存在這個(gè)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)初始化時(shí),將庫存加載進(jìn)緩存,并將秒殺商品的狀態(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);}//校驗(yàn)秒殺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);}//入隊(duì)MiaoshaMessage miaoshaMessage=new MiaoshaMessage();miaoshaMessage.setUser(user);miaoshaMessage.setGoodsId(goodsId);sender.sendMiaoshaMessage(miaoshaMessage);return Result.success(0);//0代表排隊(duì)中/*//判斷庫存GoodsVo goods = goodsService.getGoodsVoById(goodsId);//10個(gè)商品,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:排隊(duì)中* */@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)你該庫存的時(shí)候,需要將緩存中的key先刪除
/*** orderId:成功* -1:秒殺失敗* 0: 排隊(duì)中* */public long getMiaoshaResult(Long userId, long goodsId) {MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);if(order==null){//如果訂單為空,有兩種狀態(tài),排隊(duì)和庫存不足導(dǎo)致的失敗,根據(jù)標(biāo)記狀態(tài)來判斷boolean isOver = getGoodsOver(goodsId);if(isOver){return -1;}else{return 0;}}else {return order.getOrderId();}}在做減庫存操作時(shí),如果減庫存失敗,在緩存中添加一個(gè)key。因此在去查詢秒殺結(jié)果時(shí),如果訂單為空(有兩種狀態(tài),排隊(duì)和庫存不足導(dǎo)致的失敗)再根據(jù)標(biāo)記狀態(tài)(緩存中有沒有對應(yīng)的key)來判斷是那種個(gè)情況,如果有說明是庫存不足,如果沒有說明是正在排隊(duì)中。
5.訪問Nginx水平擴(kuò)展
系統(tǒng)的負(fù)載均衡nginx,如果前面沒有加緩存,單群加服務(wù)器沒有作用,全都落在db上,db并發(fā)是有限的,再加服務(wù)器也是沒用的,我們基于帶有有良好的擴(kuò)展性。
第七章安全優(yōu)化
防止惡意用戶刷我們的接口,秒殺開始之前不知道訪問那個(gè)地址,比較安全
驗(yàn)證碼作用: 1、防止機(jī)器人或工具刷
2、沒有驗(yàn)證碼,大家只是點(diǎn)擊鼠標(biāo)請求集中,數(shù)據(jù)庫壓力大 (有的話消耗時(shí)間,將瞬間的并發(fā)量分散到10s開)
接口限流防刷: 系統(tǒng)本身容量有限,防止用戶惡意刷接口,在某個(gè)時(shí)間端內(nèi)限制用戶訪問的次數(shù)。
1.秒殺接口地址隱藏
思路:秒殺開始之前,先去請求接口獲取秒殺地址
1.接口改造,帶上PathVariable參數(shù)
2.添加生成地址的接口
3.秒殺收到請求,先驗(yàn)證PathVariable
前端拿到path后在調(diào)用秒殺接口
秒殺要接受path,校驗(yàn)
怎么驗(yàn)證? get緩存redis里面的key和傳過來的path是否相等
2.數(shù)學(xué)公式驗(yàn)證碼
public boolean checkverifyCode(MiaoshaUser user, long goodsId, int verifyCode) {if(user == null || goodsId <=0) {return false;}//從緩存中取驗(yàn)證碼和輸入的比較Integer OldCode = redisService.get(MiaoshaKey.getMiaoshaVerifyCode, user.getId() + "," + goodsId, Integer.class);if(OldCode==null || OldCode-verifyCode!=0){return false;}//驗(yàn)證之后,將緩存中的驗(yàn)證碼刪除redisService.delete(MiaoshaKey.getMiaoshaVerifyCode,user.getId() + "," + goodsId);return true;}3.接口防刷
需求:設(shè)置10秒鐘內(nèi),最多請求5次,超過這個(gè)次數(shù)就算為非法請求,提示訪問太頻繁。
設(shè)計(jì):使用攔截器,將這個(gè)功能與業(yè)務(wù)代碼分離,能讓其他方法形成復(fù)用。
獲取注解上的時(shí)間,設(shè)置為緩存key的過期時(shí)間。去緩存中獲取已訪問次數(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){//提示錯(cuò)誤信息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
- 下一篇: 思维导图下载 注册安全_【思维导图】初中