1 n4 p3 ~: v, n B" ?: o页面缓存你优化主要使用redis完成了页面缓存、URL缓存和对象缓存,并且使得频繁访问的商品详情页面和订单详情页面静态化,成为纯html页面,使用ajax异步的方式与后端交互,实现前后端分离。另外还有静态资源优化和CDN优化没有实现。 0 \8 E5 w# q$ x/ p 9 ]( ]0 Y7 O6 K8 L1、页面缓存 $ q' @; i0 }0 C. A% t 5 g% k( e/ g9 C* d6 e这里是一个商品列表的展示,可以看到前后代码的区别,刚开始只是直接去数据库中获取商品列表并返回到前台的页面,前台页面获取其中的信息进行填充。而下面的代码则是对页面做了一个缓存,流程大概是这样的:从redis中取缓存->从数据库中取数据->手动渲染模板->返回html。6 j; H/ }. ^; | v4 p5 N# k. t% _, p
首先从redis中取相应的缓存,如果有直接返回,没有则到数据库中取商品列表的数据,并且进行手动渲染,渲染后将html页面存进redis中,并且返回。在redis的保存方法中设置了过期时间,默认是60秒。5 i& S' ]" T! z
2 S+ a8 Q: {, ]: T3 I0 d7 l' r@RequestMapping(value="/to_list") + G# F* l, ^- w public String list(Model model,MiaoshaUser user) {- q- w6 T" `% ?) t9 E! u; `
model.addAttribute("user", user);6 \+ n; w1 r3 }- O* t# U# t' E+ |$ g' }
List<GoodsVo> goodsList = goodsService.listGoodsVo(); ?% a% N0 g% F+ K model.addAttribute("goodsList", goodsList);; v/ q& l s; _' f- X
return "goods_list";$ T% p$ {& Z+ j9 o7 a! E1 o: \
} & J7 q: m2 P* f- @( g5 z. a1 & ~+ \2 l6 A5 D- \/ o/ `29 Q! [/ _% ~( X3 U, O9 G3 d9 [: _
3- a) |. |; x. u* A {# c6 o
4/ Y4 S: b. _. F# l# G
5" n: Q7 C0 |* v) k3 Q1 T
6 , y1 y9 {& A, z* _, z5 b4 Z: _7 ' \1 y' \! \& ^6 o [7 t( G/**+ H: i6 T5 C- Z" A9 U
* 页面缓存:取缓存、手动渲染模板和结果输出 2 s* f( q: Y# Q! }! h+ U * @param model$ q) v% i0 T9 W& N/ C6 z1 w5 l w
* @param user 1 B7 N2 e) _: U9 C* X* y * @return * m ~# q8 y2 |9 k4 k& H( r' T */ 7 x/ v/ T. ?' W9 Q) f l @RequestMapping(value = "/to_list",produces = "text/html")* \6 u5 [5 X$ _) N% O
@ResponseBody, S/ B% G0 h; E) l
public String toList(HttpServletRequest request, HttpServletResponse response, ( A2 T3 L. F6 R S0 u3 j Model model, KillsUser user){& w- R9 r6 j! w$ S
//无cookie返回登录界面 , L. m" B! n0 U0 v model.addAttribute("user",user);* |3 j& p @8 O! N
//取缓存,如果redis中有相应的缓存,直接取出使用9 {& X4 O' G7 A
String html = redisService.get(GoodsKey.getGoodsList, "", String.class);# i* D$ e' ~# u% E* u& x: m
if(!StringUtils.isEmpty(html)){ . u. C, _6 h, g: ~, O. r& T return html; 7 e# t4 A# X6 H" S# C8 A } - Y1 Y, r, P6 ` //查询商品列表 : X" R9 ^3 L% X) z! @% { List<GoodsVo> goodsList = goodsService.listGoodsVo(); # U2 f3 R- A2 @4 ?' T6 T model.addAttribute("goodsList",goodsList); ! |: N. ~ o/ a1 r' j //return "goods_list";1 Z8 o- i* j6 C1 G
3 C) [1 g+ ?, r3 t! v/ X+ u SpringWebContext ctx=new SpringWebContext(request,response,request.getServletContext(),! C4 y9 F, p7 N4 q* m1 T7 m
request.getLocale(),model.asMap(),applicationContext); ; p4 C d# Y$ T, r* D d //手动渲染模板并且存入redis,框架渲染的具体方式也是如此4 K) U. }5 F, G7 X* d6 O% P
html=thymeleafViewResolver.getTemplateEngine().process("goods_list",ctx);; Y: D5 G% U4 q) T, S0 F
if(!StringUtils.isEmpty(html)){7 h9 ~. f; r$ \; g' R+ g5 b
redisService.set(GoodsKey.getGoodsList,"",html); Q* G6 L, S5 B5 y P
}& M6 L7 b% m" o6 T; F
//结果返回 5 n! u! }0 H! Q/ |( v1 T N return html; S# N9 h) { W
}) J; h+ T+ ]% C, r
1; ^$ w4 h1 H6 A1 d' o+ O6 o
26 I) K0 `- G, C3 R0 M* e& w
3 3 M V! ?$ ^. O+ r) c4. i% p: b7 Y! g0 a( t7 [
5 2 ?+ R9 g! N$ R+ p6 8 ]$ f* H4 t. ` o1 ^3 J7: m" U" u4 V1 b- `
8 $ m4 f8 ]: W! ]# K. ~# m& L98 y4 n8 C2 n, j4 G7 X6 P9 g
10 5 c! c! f- L P) S3 y0 O; i* T11! a' l3 J0 _; S3 A; G/ e) G' `
12* ?5 y2 h* b0 w2 e
13& z7 [& O. l+ h: I, C; ]! J; I# l
14" l' S, ^% ?( R4 E$ y. @
154 o6 p! G: p z/ {0 s
169 V. l- R' i3 |5 c: p2 t; g" [$ D
17. Z [# E1 V, m* W
18, n9 ~0 f# X( G
19( G" |* ~9 r4 ?
20 ' T& ~: |. W/ {) A1 W21 & V0 p7 Y) H" T7 L! j. z22 " w, M$ Q# | ]6 s23: R' ~% @( z# p# G- g3 Y- k/ J
241 L: }( j* Z2 f+ N% n2 d2 h& _
25 * d# Q8 Y1 ^; G' Y! r26 3 P6 F( Q7 w/ b( W270 G0 n$ B" F) f- R7 {
28 + f/ R" k! w4 O; Z$ ?% N' D29: O: @, g- o2 K4 I6 B: T) D1 T) S
30 ) \+ x6 k# y w3 U% c- p1 W% W31, `3 N. p& m: _/ R8 j; s
327 m9 S8 q- {' w) w! g) v* _+ d
) f- v; t m6 P
. w+ R* o1 @: R& v! q
2、对象缓存5 s, V! S- _) \5 ^7 ?
8 @5 \6 ]3 s$ H2 i3 r
根据token对秒杀对象进行缓存,如果缓存中还有秒杀对象的信息,则不去数据库进行取出。 - ~3 k8 ^. ]% \9 U- l1 ~9 }& N; ~7 o: N; n
; r9 ~$ S8 I$ ?" D3、页面静态化 : ~; {1 G* \# a& L9 D, y 7 v4 h0 G0 ^$ q( g页面静态化常用的技术为AngularJS、Vue.js等等,此处使用js简单模拟该过程,实现前后端分离。页面静态化,其实就是把用户经常访问的页面做成一个静态的页面,避免每次都要从数据库取出数据并且渲染到对应的模板文件中,从而减少服务器的压力。 $ S9 O1 B9 o' R5 C页面静态化是将动态渲染生成的画面保存为html文件,放到静态文件服务器中。用户访问的是处理好的静态文件,而需要展示不同数据内容时,则可以在请求完静态化的页面后,在页面中向后端发送请求,获取属于用户的特殊数据。2 D. k- i3 G: U) y4 d! r
页面静态化是一个以空间换时间的操作,虽然页面增多时会占用更多的空间,但提高了网站的访问速度,提升了用户的体验,还是非常值得的。 1 M, K7 w; H0 }% m3 ^8 L% c( H关于原来的页面,正常的前端访问后端接口,获取html页面,展示,而页面静态化,是利用ajax异步传递数据,在前台进行数据的填充,实现了前后端分离。 - b3 ` [5 D+ V: r6 ?7 ~1 }& u( a+ `# j3 X* @5 ?1 c& I
@RequestMapping(value = "/to_detail/{goodsId}",produces = "text/html")" e' U: R6 D/ F- E1 L4 N
@ResponseBody( _' [9 Z" }8 i
public String detail(HttpServletRequest request, HttpServletResponse response, 0 A8 R, |8 i& f$ z7 n" C9 r Model model,KillsUser user,4 p4 l$ F6 A, |
@PathVariable("goodsId")long goodsId){$ D7 x7 }6 \3 A J# @
model.addAttribute("user",user);! E. u; H. f5 m; s
0 Q: {- \6 f/ L+ o5 C
//取缓存,如果redis中有相应的缓存,直接取出使用1 @, }' `+ R3 s: Q1 X3 N
String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class); - n$ O# n1 K4 {$ x N if(!StringUtils.isEmpty(html)){ 7 C+ c. g9 y6 Z, Y q return html;& y! v! P- j$ X& r
}$ b# R& I/ }) X7 k1 [/ O0 y6 ~
+ i; u% h) M, }3 ]/ t
GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId);3 A1 L# I0 N6 Z) k, w( k- x/ }
model.addAttribute("goods",goods);. g# g, R: P& E: M9 l+ Q& G
) m' i$ D" @6 [) T8 U& F long startAt=goods.getStartDate().getTime();6 V9 p6 K% `* G
long endAt=goods.getEndDate().getTime();" x1 T" g8 t! K f: u
long now=System.currentTimeMillis();( z) l1 a& }8 y$ @8 z
& n. r* g: g7 w. u0 ` int KillStatus=0;//表示还未秒杀! O' O4 V, A1 }- g
int remainSeconds=0; * ` M3 E0 p: Q8 s% v; n* \3 e. D2 M/ q
if(now < startAt){//秒杀未开始, Z" I7 P$ f* s8 z
KillStatus=0; 7 s6 }+ e p' p remainSeconds=(int)((startAt-now)/1000); " O; n2 ]) V! e* k }else if(now > endAt){//秒杀已结束& s" X* ]4 a. x+ H* Y
KillStatus=2; 5 h4 j) Z! u! N p4 U u remainSeconds=-1;, b" q4 b% ?9 L. w2 b' u* b4 k2 H& \* \
}else{//秒杀进行中 G2 G4 s* G- b6 `* F KillStatus=1; 1 q- r' B+ Z" g, X3 O/ m remainSeconds=0;- h; d4 M; x0 W. A
}1 T$ ^. X! M! O% d# e0 h; q
model.addAttribute("KillStatus",KillStatus); - v" n. O9 D% t1 P) [5 q model.addAttribute("remainSeconds",remainSeconds);; o0 I, j9 u8 k9 w3 O% q
. _) E" {, N+ w' X$ |3 v- }7 U
% M3 i N) r# b3 y2 i: A4 ^ SpringWebContext ctx=new SpringWebContext(request,response,request.getServletContext(),5 D7 O* u8 U6 I5 w
request.getLocale(),model.asMap(),applicationContext); 5 S7 K, @6 C$ r! }$ l, B //手动渲染模板并且存入redis,框架渲染的具体方式也是如此 . |& p+ n$ s, @: w html=thymeleafViewResolver.getTemplateEngine().process("goods_detail",ctx);- Y; a2 @ L' O, T8 L4 u" D: F0 v7 I
if(!StringUtils.isEmpty(html)){ ; A1 ~/ s! c. I3 _* x redisService.set(GoodsKey.getGoodsDetail,""+goodsId,html); ; o/ o* {' \$ U! m! O8 O0 B6 z( W } 9 F4 o' X6 I) o. |5 E ] //结果返回 $ ^! o/ ~0 l( X1 k return html;$ ^& C1 ?4 ~" W, c
//return "goods_detail"; " u* s% T, u+ L }' i: L1 O3 H' E2 z6 k- g
1) ]3 o+ X! z* Z& Y2 S K5 A3 ~
2 $ m9 |, w+ f. }! s2 j$ u! O) Q9 @; ^31 _2 L6 {( U( p; ~8 o5 l; D8 w+ p
43 C% S7 L4 X' N3 `2 x4 M8 g! Y; n9 ?
5 - ?, x! B1 V4 W5 A6 `6 & f2 e0 |3 ^: F: _7 ) a# y6 L5 \5 _9 N0 m& J2 ? P8 $ w$ n+ g+ _' S8 m9 p: o- Z( `- D# `9 9 z$ [$ e: E) N7 z% l7 {101 M& H( H' ]3 \1 R9 H
11; b6 g0 ~" |3 w
120 ~, d" d8 `1 R# j9 k6 d" W
134 {0 [- n; a2 j! P/ m6 v
14$ u# Z4 U# `2 w
15 ) M4 v- {/ s6 p+ m( p16: b; m, I$ _& _+ ^ F, [
17 $ }$ h6 s V% |187 O3 o1 N* \" E t: I9 {
192 U. S& y/ m& j9 _9 w% _: ^1 {
205 M! w4 o H6 L/ y! t0 f! N
213 o5 P) c+ ~& W H
226 [3 |: p9 ^, |* X5 {
23 6 q+ Y( a! I7 b/ e/ k+ M24 , S7 @) l4 F: N0 v6 f+ r3 H25/ Y1 v5 B6 i, F( d
26& J& F* c; X: C! C7 x
27 4 b5 @/ V& D4 p+ s* b6 T9 r28 % l2 u* h- E' N* h8 f$ C294 V# g6 U9 O% m& P8 ^
30 ) G( y+ y o0 S% W5 X8 W; e2 v31% G+ I9 H8 z) s6 O7 j1 l" P
32 & K& f/ H* F2 ~4 r33% d [" A( l6 f- y7 F8 N( [8 G' x
34 P! B3 x9 ]9 U$ C
35, E1 y# h+ b6 J8 n
36 : i, C6 E& K) R37. |9 [( i7 k7 t; w* `! ^6 g$ \
38 * g j$ |+ X: U; W) p# N2 q39 / \9 q3 C4 V& K5 \3 n% x1 [. Q0 {40 7 m7 i+ W4 ?- Z6 c* _5 e) Q0 b- @41 ' T1 }' K3 q& ` c% F42 : h, _- A4 s" @- `# J434 c. L! }- _# X, ]4 H
44 $ n" i. v. b& ]2 _, J9 ?45 9 e' F, ]8 o2 D5 F: w O46 n; y) G0 ^+ f" ?7 z
47( O' V2 V/ X- B$ X
48 3 k, a" w. F% Y* Q/ C) P前端代码通过ajax异步请求后端的接口返回数据,并在前台取出数据对页面进行填充。; G: j: Q M- W: v7 N0 f
- K+ y: u* c8 P6 D- J( h1 N! e W
//前端- \! e7 ^$ q2 C: Q5 b$ j
$(function () {6 [3 A4 I) {1 i. V# B
//countDown(); 4 r& J" K/ M3 D* q' Z getDetail(); 3 w+ ?) @3 P; q4 o}); 0 m4 v- E" M! n% N% a( x6 b: @function getDetail(){, L) ~: r/ { y2 v
var goodsId=g_getQueryString("goodsId"); $ w0 |4 t' I5 V$ { G $.ajax({7 f4 }! F9 C0 R
url:"/goods/detail2/"+goodsId, 1 v/ U. C% t$ Z( g type:"GET",0 _+ y! }, e: H7 {" |& l9 [+ M
success:function(data){( ]1 Z6 a3 v/ Y
if(data.code==0){ ) f5 g$ E6 Z. L" y0 B render(data.data); # `7 e$ n' G7 K \+ p4 Y* e' U }else{$ Z+ }, |* d8 _" U' n$ N5 ~
layer.msg(data.msg);& M& [" X# y9 F/ I; N
} : g* \ ]5 j1 r$ Y! [0 k* M },' I4 |$ I0 N0 b+ |3 Q. o4 o
error:function(){% l! B: K4 r( D
layer.msg("客户端请求有误")# \; T4 W2 c; E4 f
} 4 J2 k- N9 B$ ~8 f% \0 k6 _. w- ]' h8 I! h
}); ( l' ?% G" l3 P) ]: Z7 R} , o( i- M, Z' e: B/*text() :设置或返回匹配元素的内容) f9 n( p, J" ]. ~% M
* val() :设置或返回匹配元素的值3 [. @6 x& w* f) ~/ Q& z
* */* v5 R' o6 V! |9 d
function render(detail){ * A, S6 T& X8 O0 I4 y `5 a var goods=detail.goods; ; ]0 \2 K& l9 v4 S5 O var KillStatus=detail.KillStatus;# @+ U; {3 G* M, v
var remainSeconds=detail.remainSeconds;) L- \* ]. Y% N1 p' b7 Z; z) }
var user=detail.user;& @( }; a' c- f& \/ \0 s4 ?$ g
if(user){! D8 G: L0 k+ g% g6 o0 h( {+ s, j
$("#userTip").hide(); ) c( e/ v2 g. \7 A+ J } E0 j3 m, P$ c5 l1 ~
$("#goodsName").text(goods.goodsName);) j/ p8 Z4 G- `8 l7 M7 n
$("#goodsImg").attr("src",goods.goodsImg);& w6 M" }0 m2 W$ K. e
$("#startDate").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss")) . T# n$ d, c8 }0 P: G $("#remainSeconds").val(remainSeconds);9 G5 l( @( {& X7 v z4 ?# J. g
$("#goodsId").val(goods.id);7 V% E# {: |# @0 |, m
$("#goodsPrice").text(goods.goodsPrice); 6 B2 J# p, S7 b7 @ $("#miaoshaPrice").text(goods.miaoshaPrice);2 v8 c" R% a% z; X/ X7 D8 [5 W+ }
$("#stockCount").text(goods.stockCount);1 X- i/ r5 Q2 y- y
countDown(); / G; Y' v, a- u5 N} 9 J. \2 Q& X; B//后台 k$ V( z( g" F7 i Q6 ]& A
@RequestMapping(value = "/detail2/{goodsId}") # ^$ e0 k0 B: G; N2 w: ^# _ @ResponseBody5 [9 n0 O" p% q4 {2 b, d, u
public Result<GoodsdetailVo> detail2(HttpServletRequest request, HttpServletResponse response, * F& q1 x8 a! \6 `* O- _ Model model, KillsUser user, 8 Z' r* ]; N8 W v( {, T @PathVariable("goodsId")long goodsId){ * W N% t2 D* l8 ] GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId);. C4 i3 }: `, U* ]2 o& Q+ o# `6 n
/ l8 ]7 P9 V) K# G# e4 `
long startAt=goods.getStartDate().getTime(); {" A* y$ q# v! V/ E) p1 ^8 J
long endAt=goods.getEndDate().getTime(); 9 k; z& ] w) W long now=System.currentTimeMillis(); ( V1 V7 r. p3 V( k5 ], Y8 ^! |! w. Y
int KillStatus=0;//表示还未秒杀2 B+ L/ z* t& z. }0 ~1 g7 M
int remainSeconds=0; 1 c: p- \& G ~9 q4 J$ w: \$ l- @" I8 D- i$ X5 W
if(now < startAt){//秒杀未开始 p k& [0 c5 L% ?0 X3 [8 x' K7 l/ I
KillStatus=0; ) }+ \: ?' \- P; O remainSeconds=(int)((startAt-now)/1000); 5 P# \7 m* E( Q! ~ }else if(now > endAt){//秒杀已结束8 `, F3 h- {1 U" O- S6 W
KillStatus=2;3 ]& F7 N0 U5 y( G8 t7 C
remainSeconds=-1;1 Z' V/ J' i* i9 ^. ]8 Q7 P, D, u
}else{//秒杀进行中 ! Q( K5 ~4 u# ~ Q$ H KillStatus=1;2 `! p# [& [* d, A! K" O |
remainSeconds=0;3 k P+ ?6 l" {2 `. \9 g+ ~
}3 y C$ f+ q; `4 x
! B) w6 a8 o5 U* |$ u2 j. p( e3 W
GoodsdetailVo vo=new GoodsdetailVo();/ {+ e. e" H. s. v0 p/ J' G6 p
vo.setGoods(goods); , u" r% Z8 G- R7 A: `5 ]9 k. l _% w vo.setKillStatus(KillStatus); 6 S2 I. S' C3 `' \% j5 w4 `3 ] vo.setRemainSeconds(remainSeconds); Q+ ]: {* c% e
vo.setUser(user); # G, S* G$ O3 N4 y9 N/ o return Result.success(vo);& |: J8 P: v5 ?7 m) Q- M2 J: l. L
}+ |3 u, ?0 k% _0 T
1' I' K/ a/ P- q. d% w/ i2 F# C
2 7 M; L/ ?5 O1 `0 R/ f8 ?% V3' _# W" z, }& i" n' ]
4 , d$ {; L% _" E3 G/ r5* K. k Z& H) t1 a2 x' |* n2 N
62 |0 Y1 _0 ] d' C; x* d9 N
7 ( K( y* j2 s8 V3 a$ _" D! T* K86 F' O" n9 E3 C& f2 t
91 Y) N1 m/ V* U% Z4 w
10 $ C+ ?% S {2 K& ~4 {11% ?# ^! Z8 ^# X$ w- z5 y6 J
12 # u) k7 y& V% m134 D f" r C1 H: ?, I
14 / Y$ M* `7 `! `4 O4 N k15 0 p6 n7 d. [# t# O3 W16- c1 a5 W! c# s* T
17 J! t/ r, o! v* X" e2 b) J* A( U+ s- @18 % I' r- G) L' v/ _) ], e4 n196 |" l2 L1 y9 T' K9 T
20; Z8 x4 {; k X8 b
212 a, m2 ]5 g2 L% w
22 1 m3 C( ~! C9 H5 q- Y: O0 ?2 B" Y23 3 w6 {4 P0 D+ V4 U8 v247 B ?/ P4 L) c2 t- G
25 1 L/ N5 L% A0 L6 w8 L26 ; W5 l* T" Y; o# G278 G4 z) x8 q" @5 T
28 * Y9 A6 D) h# F1 A: Q7 ~29 & q) H/ U2 s" t30 9 t$ H2 R3 [% j! a* }31/ ^: k1 M. T3 Q& \
320 v/ s8 \7 j" \- K7 i0 u
33 6 U' R* |- \( M" e% G34 8 j; T$ Q8 S' C: _35 3 ^. X: I, c5 R0 b) \' \36 & Y# X8 r" M# _ R37 2 _; F( b7 F7 a; ?38# q7 F0 |7 H5 _# W
39 6 e' ?7 z% Q8 H8 ?40 ; U' _, a7 w' x. g& M" H8 Y413 E8 _* y" y: f3 O G/ F( ^
42 ; G0 }/ `* R% M0 }7 l431 f2 U# i9 r+ ], t" p* e
44 1 {3 ~: }: u! G h1 d- ]45/ x# ?" P5 h6 ]
46; h1 I- Q* {) @) E8 D& L
470 }) y, `# J- {( ?. h( f* ^
48 W2 x) i" T3 f6 r8 V
49 9 V* l# ^& S; B; A50. P& t K, c5 ?& ?# i& Q
511 e0 t- ~: _1 F4 ~
525 B' M% V \" n% u4 }
53 8 Q- K/ J7 N% q* z54 |) C( L1 T9 ]55. P+ U, S+ U9 r% G
56 # c2 L4 f9 h" C4 a57 . X0 F( T |/ e' U7 y3 r: g58 & g8 W7 \, \1 i: U- T7 z59& }. R; y1 W9 S+ A3 U4 K7 F, T% |) r+ c
60: J+ h3 v' K/ O1 z! E7 F6 q7 ?' A
61 , D9 A( ? ?; K) [ w( A6 |+ U" E62% T% U# e" S, I) T! z
63 6 ?/ z# L [0 P, Y1 ~+ w! x# r& x: j648 p# I2 x: l! b' D: G
65 & L( e$ k5 d- D6 O66 ; e/ z. o& k' K- a1 a67 8 z4 j- x m* I g68 " N3 w" h4 F6 t% a. Q) O69 & B! Y: x$ m2 \2 I) r' E706 J: c+ f- p% N! R& v/ l7 f
71 ( r0 q1 A( [. g/ D* Q+ z72 1 M' U( X8 H* z6 L# [3 i1 m73 8 | a2 B3 W' A74 1 ?( O A& C9 @# A75 g% d. o' l, [6 b) }; j7 R764 t. A, e8 g' K
77' M, _: V8 g+ g) A3 Z! n1 [
四、接口优化 , ^6 s/ M6 ~8 J& @ & _, w- {; z1 h& ?9 C' d接下来是对于秒杀接口的优化,这是最初始的秒杀接口:" L- m* O5 a' l1 a& G- G0 q
刚开始的秒杀接口的业务流程是这样的:判断用户状态->获取商品库存并判断->获取秒杀订单并判断->减库存。这个过程需要调用数据库共3次。. U2 h1 ]" [4 @( Y5 K) P9 p! O, i: k& h: _
秒杀需要面对一个很严峻的问题:如何防止超卖或者少卖?此项目中总共用了两种方式,第一种是在减库存的sql语句中加上对库存的判断,来防止库存变成负数;第二种是对数据库中的秒杀订单表中的用户id列和商品id列加上唯一索引,防止用户重复购买。 & ]7 w& q P! E: | p' i5 F8 k ' m7 d' i9 \3 u3 J2 y; U: h( w% W@RequestMapping("/do_miaosha")2 M2 k3 K9 B5 r: Q; j6 g9 ~9 r
public String miaosha(Model model, KillsUser user, & [8 M. s3 x0 L% `" b. V @RequestParam("goodsId")long goodsId){ 9 E! z. [) i( n1 u/ Q& L- z( F model.addAttribute("user",user); # N7 ^) E7 s/ L) D: g0 ^5 [ if(user==null){ + ?/ V) H7 A& K% y1 w1 M7 T4 k5 Q" }# z return "login"; ; }0 Y4 h( \" [8 l. n) U: I } . L5 w7 U5 j5 O/ j GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId); # }+ U F/ u" r/ W( r int stockCount = goodsVo.getStockCount();5 N4 E2 }* N! X/ I& N' B
//库存不足7 O. f6 A2 K u" Z. Q3 Z/ {
if(stockCount<=0){8 y' E* [$ ^: Q$ o- o
model.addAttribute("errormsg", CodeMsg.MIAO_SHA_OVER.getMsg());! t$ s+ B* K4 d3 ?5 ^& t
return "miaosha_fail"; 6 Q# m+ M; r, E5 n+ D/ [( d }" s- ~9 T ^1 G
//判断是否已经秒杀到了) j6 I4 a% ?5 t: @8 y; h0 _4 _& X( b
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); $ @8 `- D, _: p$ ~" I2 t( q if(order!=null){ ' p1 Y- \( I0 G& q# M& c+ k model.addAttribute("errormsg",CodeMsg.REPEATE_MIAOSHA.getMsg()); / [! B" {. A1 t* A% M- U return "miaosha_fail";1 O5 R; w( [1 w0 n) y& z
}! x" U- h$ K" E; X
//减库存,下单,写入秒杀订单* l7 V( V9 r) h$ p/ J6 l' n' O: J
OrderInfo orderInfo = miaoshaService.miaosha(user, goodsVo);" X+ u3 {- a7 J) r
model.addAttribute("orderInfo",orderInfo); L/ X/ `7 I( U; H5 p
model.addAttribute("goods",goodsVo);: w* v t+ @& \. s3 Y3 ^
return "order_detail"; 9 w5 K0 ^7 V% E5 L$ t" p- C } % U( A% m+ h& u6 z. b1 5 k$ e/ R O/ ^- s2! S% N3 K I: S8 b/ {1 i! N
3 0 G4 j0 X" R- e) K! H) E2 K5 V48 r8 X5 S- B+ R. J1 J
5/ c9 F/ r2 p) O7 y5 i: c6 P2 C' P
6 9 h+ w$ V# i# P X" n$ Z) e7- b8 ]) w9 O! g6 X( z2 H" P+ j
8 K. M# e( @& c& H/ Z
9% C5 V. o6 E2 P t0 x* L
10- q2 F' g5 V( m$ h4 h
11 {( y D U$ z; p/ |+ g# o
12) i5 ?) Q" _, Y, U! c7 i, D
13 ) w$ H' R( n6 k) E8 p9 ]14$ b+ A' x" H" [/ w* ?1 m; x; f
15) q- t: s4 o5 ~2 I% B. Z" f
16 + o# }1 s7 X0 P5 l8 {* N. y17! o- d, Y( v0 a) U
18 % v0 ?3 ?- w" w; ]8 E19 $ t8 o; b" V6 S9 F20; H: Y$ M; G" n4 v+ V
21& O# E/ I8 g7 f3 C
224 s, R+ }; x8 X
23 7 l% ]+ f Y* ?24 K3 m6 r. ^8 [- s" @1 G4 ]
257 [8 j$ O1 K* ]/ _
26 $ A, X N- ?2 D$ N Q优化:秒杀静态化,通过ajax异步请求调用秒杀接口并进行轮询查看是否成功,redis预减库存减少对数据库的访问,内存标记减少redis访问,RabbitMQ队列缓冲,异步下单,增强用户体验,同时减少服务器压力。首先是使用redis预减库存,我们使用一个HashMap来存储某商品是否售空,这里用到一个方法afterPropertiesSet(),该方法会在Bean的所有属性初始化完成后调用,在这个方法中,我们从数据库中查出商品列表,将每个商品的库存存入redis中,并将库存不为0的商品在HashMap中设置为false,意为未售空。(此处应加上对商品库存是否大于0的判断) % C. O$ H7 H% |7 U+ u4 u; F4 r在秒杀方法中,我们首先获取mao中的布尔值来判断是否售空决定是否进行下一步操作。使用redis的decr()来预减库存,减完判断是否小于0,如果小于0就设置map中的值为true,并且返回,否则进行下一步操作。 K$ i1 y _+ v: {0 K" z" U关于RabbitMQ的操作,我个人感觉还是有点迷的,毕竟真正的秒杀不会让你一直等待吧?(没事,咱也没秒杀到过东西,问题应该不是很大,也可能等待的时间很短人们感受不出来?这里请懂的朋友评论区指导指导呗). p2 ~# @5 ~: k
此项目中使用RabbitMQ将秒杀请求入队,前端进行轮询确认是否下单成功来实现异步下单的操作。关于RabbitMQ,RabbitMQ是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,或者简单的将作业排队以便让分布式服务器进行处理。 1 H% @6 b1 q: y0 g* kRabbitMQ有四种交换机模式,分别是Direct模式、Topic模式、Fanout模式和Headers模式。Direct模式需要将一个队列绑定到交换机上,要求该信息与一个特定的路由键完全匹配;Topic模式是将路由键和某模式进行匹配,队列需要绑定到一个模式上;Fanout模式则是不处理路由键,只需要将队列绑定到交换机上,一个发送到该交换机的信息都会被转发到与该交换机绑定的所有队列上,类似于子网传播;Headers则是以key-value形式来匹配。7 n0 F1 b2 C1 e G2 I
队列和交换机绑定使用的是BindingBuilder.bind(队列).to(交换机)2 t, u2 ]' k+ ?- P
6 [% h( T0 A- y, I. I& B注意此时秒杀操作并不在秒杀功能中处理,而是在RabbitMQ中的receive()方法中处理(见第三段代码)) ]% ^4 f" Z9 c. A8 Q. Y# T% f
) ]7 k1 l' u2 x- I+ y) x/ B6 g
//用于保存商品的id和是否售空 ' g& y0 V7 G' A: a3 h" _# Q private HashMap<Long, Boolean> localOverMap = new HashMap<Long, Boolean>();1 j$ O- ]+ Q& F1 J+ X2 N' A5 i
, x0 v1 o" A" e- m% `% A2 R /**( b, W+ ^3 o+ {4 R! X2 J5 I* Z
* 系统初始化 7 ]- v' K! Y& @9 p * */3 @7 V& x9 a+ C
public void afterPropertiesSet() throws Exception {/ [' Q# h; x& `" l
List<GoodsVo> goodsList = goodsService.listGoodsVo(); " t- j) P3 k9 J5 d) S4 N' f v% a' z+ c if(goodsList == null) {( k4 V* w% B3 O+ h
return; / t# Q+ H9 U; f% m- Z) s }0 O9 X; e( F+ o2 h$ `
for(GoodsVo goods : goodsList) { 8 q# e7 t' b. ?0 h7 Y redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount()); * t* {: J. _8 S! Z7 G- D7 n3 u localOverMap.put(goods.getId(), false); - A& O: P. w. o! `; [ } , f7 h1 f4 u' ]( |2 V9 u } 4 p. P- x& F; X$ L- O0 L6 E& E( l: |) o+ f5 d
- E8 ~( b- e" z4 t. C% J @RequestMapping(value="/do_miaosha", method=RequestMethod.POST) + q/ l$ y. P' |' z" m @ResponseBody& e4 v# Z( v0 C( o- R7 W; Z
public Result<Integer> miaosha(Model model,MiaoshaUser user, $ K9 _7 |0 x3 S% P* h9 n+ O$ { @RequestParam("goodsId")long goodsId) { ) ~5 y: ?( M) X" e% Q' U+ a& o. S9 g; y8 G model.addAttribute("user", user); ! h$ ^8 }2 p. o5 v0 t) C1 k if(user == null) {+ J' w B* ?& [! D% f
return Result.error(CodeMsg.SESSION_ERROR); # M8 w1 F8 a( I: s } . v& f! n5 E) g; d% s9 { //内存标记,减少redis访问$ l! l3 ` m" Y/ Z+ j# U3 c
boolean over = localOverMap.get(goodsId);4 q: ^3 n6 A8 L5 y3 p, K
if(over) { / e! S1 H$ W9 A9 l. V" \3 L( y0 F return Result.error(CodeMsg.MIAO_SHA_OVER);9 ]* s8 x/ N4 W5 W( F6 B
} 1 O7 b9 ?6 v' b0 S, W //预减库存, ~& Y# \( d& P1 q( u+ g) _
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10 2 F4 {2 P% Q, o& M6 c( X2 e if(stock < 0) {1 u' J6 q; V6 N# P! {1 O
localOverMap.put(goodsId, true);' Q+ |4 \5 m& C1 Z. C3 ~
return Result.error(CodeMsg.MIAO_SHA_OVER);! S' k, U# y. X' |* u
}2 B' I2 |+ h: z; A. _) F3 Z
//判断是否已经秒杀到了 3 O( F; ?* M. I& c" r: J4 ` MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); 3 {* A$ O, l( \2 [ if(order != null) {8 Y+ r1 D' g1 K9 s5 s$ Y
return Result.error(CodeMsg.REPEATE_MIAOSHA); * d {% V9 V3 N; c. P/ G! \ } / C$ D' I G2 O2 O+ W, j v //入队 5 H* z( n' w9 z* I$ a MiaoshaMessage mm = new MiaoshaMessage(); 2 }3 z. [5 G5 _# J) }7 ` mm.setUser(user); 6 x7 u' e5 L7 n mm.setGoodsId(goodsId);- ~$ b2 w! Y" E0 q T. ^
sender.sendMiaoshaMessage(mm); * t! _$ r( J4 s' x. V) o return Result.success(0);//排队中! A& r3 w9 g: f* E0 |% }, P
}! N: @' J- p2 X% f5 w
6 j( u( a" Y' `% N! h, G& g0 J2 B
/**MQReceiver类中的方法+ J' G+ K8 F% d, ~* B& ^! t
* 此时到达这里的请求大大减少8 x8 E8 @! f6 U# W0 s9 j2 m) T
* 因为有redis库存的拦截: o$ p' c% Q( D! F# s
* @param message " V5 U7 q9 p% _% P. F* U5 ~ */ 7 y! }2 [% a7 c @RabbitListener(queues = MQConfig.MIAOSHA_QUEUE) - h! O% _, y3 [% y9 w3 D; r public void receive(String message){ 7 N0 p! M6 S9 k F& ~ f log.info("receive message:"+message);$ d- C, @; y1 A6 Q
MiaoshaMessage mm = redisService.stringToBean(message, MiaoshaMessage.class);' ] r9 u$ U! h5 g) V
KillsUser user = mm.getUser();) r1 A8 P, @5 |9 _" k5 W1 G0 d
long goodsId = mm.getGoodsId();% O& F6 o) H) p) c
//判断库存: p3 D' \0 S k1 v. B* R
GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);2 J+ R8 \# E, a- F8 @+ S
int stockCount = goodsVo.getStockCount();/ q5 }9 ^1 w( L. U. M
//库存不足 % @$ @1 U& K, q) x2 ^& x& ]* a" c if(stockCount<=0){ 5 z; v8 ?% L5 j return; ) w/ T% c, B0 T. t# k }1 @4 {) p- |. P) a
//判断是否已经秒杀到了,这里可以不去数据库查找是否有订单 ]9 n* ]: H3 y2 `/ z2 h) b MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); ( a) t7 c# l4 d9 \ if(order!=null){ H. o$ t+ |4 ^/ p5 E+ [
return;, y$ k+ B1 h! R7 C' W
} , ]0 }' R3 n+ g" B C7 B+ Q //减库存,下单,写入秒杀订单 $ q8 w( y8 v* }% Y$ y+ L OrderInfo orderInfo = miaoshaService.miaosha(user, goodsVo);: v3 n! k2 ]/ o5 }4 B0 S" q$ R
}7 i* R+ x r+ U8 X3 E# B O7 i
1 ! z6 t6 c2 K# r; t! f0 s( V/ I5 S! x2 # J' ?) y; W+ y" v+ W/ I' r3, c* x9 w2 P7 `& P+ |% z4 B: ?: r
4 3 T! M! P1 u6 l6 n1 b) v9 b2 n- R5 : E& z/ {9 Q) t8 ]) V6 ! {4 N# i: r$ G* ?% T0 [4 N" y/ [7 ) I! Y! b4 n5 H* r5 i8; T3 p, C6 R! s1 F8 L
9 - q6 _7 E0 ?& y4 P7 c- ^10" E0 M4 v K% D# G8 i6 b8 P
11 B- ~& S7 [4 n3 y123 w; G* a+ j( I/ q/ T6 n. o- {9 j
130 D1 y' t+ \1 E
14/ T* n% O j# }3 c
15 ; b/ Y3 r b" u* u" t! a16 : E7 h, W6 K8 s175 Y: Z2 _/ @1 [0 C- l: t
18- a2 _: W; W4 Z8 o
190 _ ]% R' w8 L. W c' X
20 + ~1 ^8 d8 G+ x1 T219 K1 }; [! y& z! d
22* b7 Z2 {; w" L { g8 ]) x+ Q
23 " `) }! L" M/ V0 {. g" R24 1 A; m: c4 S" S, h! b3 L4 P25 ! K1 f: J/ Y) s. X% n4 c3 P26& X \. v4 B# }. r3 \
27$ Q$ U U( l3 S, w
28 , t9 n9 n9 P# E- ?0 l6 R29 8 m3 E8 L! ?* E3 J( }# M; L3 x$ ]) _30 ' c! M# l. J: N; S( ~ x! _31! Q; Z5 l1 d6 C3 h8 ~6 C2 V, ?
32" ^& G" }4 m. J9 B/ c
33 , [4 \! Y6 v% Q# I& C+ b. w4 ^0 P34) H* e% `. m! T$ v4 h0 P
35 H% r- q8 P$ i# A+ u5 N, ~36 * Y, m- N+ f; I+ k( i% J# x37 $ `* c; s) f; T5 w' d. k& ?38 + `2 c, ~9 }6 g- l' X7 t39 6 p+ q9 l, m0 I6 F) j40 ) g6 q3 v" b$ k+ a41 - f5 F3 E# p% g' J9 ~9 r! m& f/ a; \2 J42 u" x w$ q6 R% F7 {6 f8 Y( k% c0 H
43( q1 L& M& i& |1 F; \* ~% }6 {9 i
44 . g( u* m4 n. S& ^6 L0 t45 1 y2 D7 W% ?6 u& R, }46 + }) \6 v- \) v0 i* O: {479 w3 S. m2 Q5 Z; x
48# R1 l0 A5 ^* R. x
49 1 W" Y( _( K$ `# T( L, H50$ ]. \" u* n$ H% p( J7 ]6 V
51 ) Q' o, Y4 J* m1 F e+ Z( @# I52 0 K$ P3 \9 Z) K2 E53 . S2 i3 c" |7 z548 L5 }, B3 q/ J: N. a5 \9 [/ U; O
55 0 I7 {( I5 H, Z7 w4 R56 3 ~+ c) s4 z5 \- A$ i57 3 B4 t' i$ j9 c% i; I589 e) ]! i- g# w
59" L4 U7 y$ k1 ?( t8 O3 w
60 4 t: |8 w" C( K+ z$ P1 M$ i61 ' v* |5 W! ~/ t) C62/ E' q* s/ _$ M9 X
63& ~5 J1 B- ^0 a2 b. H
64& O* {3 T3 e5 b/ k9 w* {9 ^
65! N$ k0 H. V0 m) }+ M
66 - W1 [8 \5 a$ {5 s67 1 V f1 n' `( j6 Z& b68 # Q( b/ b/ E$ i69 : {2 C9 A: i/ X: X70 ) B" y; m7 G5 n3 G71; _" {6 i% e4 i" e! R
72 c6 O1 I, T) K& Q
73 6 M2 ~7 T5 o9 g; B74 / V y$ G) F) e75, k: D2 ~3 u n! |
76 " b. B4 D7 d$ Q; h五、安全优化0 Y+ I, I) e( x' V& ?' g% D, l
! J& V, _& p% t& p" d4 |
1、秒杀接口地址隐藏 1 H4 c! ]3 E. W& o1 e5 `+ C9 p) B [3 Y7 q: }
在秒杀真正开始之前,我们的秒杀地址不应该是可以访问的,这样会造成极大的不公平,如果你获得秒杀地址就可以在秒杀开始之前买到商品。因此我们可以在地址中间加上一个随机的path,可以利用随机生成随机码,并把用户id和商品id作为key,把path存入redis中,在真正秒杀之前会再度把传入的path和redis中的path作比较。8 b6 f+ d4 y Z* W7 J, K# ^& a