9 F6 [. S. ^0 e" w$ { @RequestMapping("/do_login") " P" n! n* q4 P2 G# R @ResponseBody' ^' j& C3 o8 v: V9 W$ t9 d7 ~2 d
public Result<String> doLogin(HttpServletResponse response, @Valid LoginVo loginVo){0 j4 H2 F: T- x( U( c
log.info(loginVo.toString()); $ `7 x6 h3 k, t8 ^3 ~! o% d! r return null;; x- Y/ j3 R7 Q
} ) k& G7 N- \' A1 & d' [5 {; R* s f# K# r" |27 p, ]4 Q! ^' h; J
3 ) P& e# @* V. S( ?0 u8 q D- S4 / U k: E6 E) ^, U8 `5) c, J7 z m6 a
6 ) f! i7 A5 \# D, t. |实体类中的注解来校验是否为空等等,其中的isMobile是自己定义的。 & w, [5 w. R9 J+ B* ~. Q0 h" y2 s7 f; L% i U6 o1 F; F W9 t! s& |) A
public class LoginVo {' A \7 C- T, {6 ]0 o6 X' F
@NotNull9 A6 Z" r" n3 v5 a: |% D
@isMobile ! W8 W2 I; {+ g% D4 }: W5 q private String mobile;5 U7 D( N, z8 S! H- f! x" O
% U/ | O$ n& w0 R
@NotNull- Z4 |+ r/ Y0 b% U, M2 S& U
@Length(min=6)! W) d1 j# h4 _* \4 t, t6 x9 A9 r
private String password; 5 n& g1 b2 d4 }# W+ X} 3 {$ G& l0 v0 ]# K; g2 b& L3 E1 M13 k# z& q- n8 V- N- h+ A* k. E
2 9 V w5 m2 q. h* m8 \+ O' r34 m/ P. ]$ I4 n. O+ k7 m- w' K
4# ?- |6 ?; j$ l2 M4 g9 d
5 3 S* @( z$ Q: _ m$ h6 / l* J# b0 N# I: X$ \3 b' D+ q v7 7 x! E0 V u8 h( x3 U, P8 % r5 ?. ]& J: |3 B5 K E% Z9 9 L( F; O2 k3 M g@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})- F/ g3 ^- w* V# u8 f
@Retention(RetentionPolicy.RUNTIME) . b6 ]. G# ?! V" \@Documented $ i% i2 n9 J9 _5 k) m1 u@Constraint(validatedBy = {isMobileValidator.class})! Y6 G$ @: @/ s( Z9 ?5 X: m! F
public @interface isMobile { 5 U r( a& Y" y X+ A boolean required() default true; & e! v; T* O, n6 b7 s# ? \# t& q- P
String message() default "手机号格式错误"; - @3 F, s* J2 c) X) v( c . s1 L5 l) o. x8 N/ s Class<?>[] groups() default {}; * x9 h$ ~! U9 A- H0 f: {* e" G: ~0 }, }
Class<? extends Payload>[] payload() default {};- N1 `! f9 q2 E7 y
}7 K5 p. K% T: ?% I
11 r& i; [+ ]: ^6 p
2 ; C, A$ }( y* C5 ^# q7 ~. r8 J, |3 & O, d i8 s; q4 I9 l( z. T4 0 s9 Z# H8 | r. i) C* P; U7 W6 W5 5 F7 b, t5 B6 G! e6 ^0 D6& {: e2 {8 q* w. r+ R
7 , {; _1 P" _% G88 @! N+ D% i& b+ n1 Z" T6 m+ \
9 % i9 k. c: I5 r- }) n+ y9 i7 W109 {. [8 _( H/ E% L. o$ O
11 " D' h8 z6 m8 u121 q" R& P( E' ~, M2 J
137 B; V* C k2 ?0 r
2、拦截器的使用0 w9 {6 d' X6 U1 e, ^; L; _
2 ~# V* y1 Z2 N, V* A* z
使用拦截器与实现了HandlerMethodArgumentResolver接口的类从session中获取user对象,并作为参数注入Controller中。) e/ i5 k5 `3 V& R3 [
& n" R8 W9 _9 D7 z8 x3、分布式Session的使用7 u! j3 P, L" i4 e' R: p' c
3 c# n8 A. f+ z# G: m/ ?* h此处分布式session的实现主要是在登录的过程中,为用户随机生成一个token,并保存到数据库中,随后封装成cookie返回前端,并且,获取用户也由这个cookie中的token来实现。关于分布式Session一般有4种解决方案,详细可参考这篇文章:分布式Session的4种解决方案。项目中所使用的就是其中的利用cookie记录Session。 ; F r5 ]2 c0 p0 {. m7 k: A4 B ) ?, h% w6 a% \$ g9 ]三、页面缓存优化 0 _ M& q( j0 b7 v0 L! L+ t# a' u4 v4 \
页面缓存你优化主要使用redis完成了页面缓存、URL缓存和对象缓存,并且使得频繁访问的商品详情页面和订单详情页面静态化,成为纯html页面,使用ajax异步的方式与后端交互,实现前后端分离。另外还有静态资源优化和CDN优化没有实现。 % k( b* @- U' K" u0 M8 @" b! Y6 D) n# s& D3 d- a
1、页面缓存; a3 B9 u5 M) E$ h
* j& J+ M$ h! X
这里是一个商品列表的展示,可以看到前后代码的区别,刚开始只是直接去数据库中获取商品列表并返回到前台的页面,前台页面获取其中的信息进行填充。而下面的代码则是对页面做了一个缓存,流程大概是这样的:从redis中取缓存->从数据库中取数据->手动渲染模板->返回html。 ' x% M: X- ~8 i首先从redis中取相应的缓存,如果有直接返回,没有则到数据库中取商品列表的数据,并且进行手动渲染,渲染后将html页面存进redis中,并且返回。在redis的保存方法中设置了过期时间,默认是60秒。- G& b7 N8 J; e# ?! ]" V7 g
$ ~1 m, `0 y$ \# P( t& W@RequestMapping(value="/to_list")6 _" a5 s3 u: O# t+ n; ~" I# E
public String list(Model model,MiaoshaUser user) {+ B2 l) A3 ? _8 U k
model.addAttribute("user", user); 5 Z# |, A; B3 ~ List<GoodsVo> goodsList = goodsService.listGoodsVo();! a' R [2 K* e0 |
model.addAttribute("goodsList", goodsList); 7 D/ j6 d8 ~: h' X9 u return "goods_list";+ l( m o0 V/ ]( ~, N
}9 D7 w8 c5 `$ I2 m5 V, V
1 9 \/ t( y9 V" A1 M; |# x0 h2) M$ @# `# } ^/ P4 `; }
35 c h6 F8 v. \- w8 \- {
4& I( A% v1 a& [* j) i; n* c
5 4 R5 y3 B; r4 \* J# h6, }# X' j ~$ o+ l, X8 q
74 o2 J0 H5 j5 x1 i
/**: q( G. [: ]/ l0 \. ]" e5 h
* 页面缓存:取缓存、手动渲染模板和结果输出 O4 P2 i) n( ^2 c1 I6 }% }3 P& Z) A
* @param model # R1 m* @; b# D. ~: L/ x * @param user 0 f9 A( x) v% C. a- N * @return * Z' j) `- i$ W; h+ p* S */9 K* Y# S" O5 k6 K
@RequestMapping(value = "/to_list",produces = "text/html")! ^! ]9 @( C5 j4 ?$ |# ^5 }
@ResponseBody V# \9 D9 } n4 l |! | public String toList(HttpServletRequest request, HttpServletResponse response, 5 K1 O# h; l- D3 r* [, P( j$ _/ ^ Model model, KillsUser user){ 3 i! ^. T) ]+ V5 `0 I7 k6 k" C& \& ] //无cookie返回登录界面" a$ a7 {4 u( R& v/ [& U9 |$ ]
model.addAttribute("user",user);% X* f# l* ~. U$ C' K2 P8 o
//取缓存,如果redis中有相应的缓存,直接取出使用 6 S8 |' [8 f) O) N8 R2 y0 U3 [ String html = redisService.get(GoodsKey.getGoodsList, "", String.class);, I" G& P( s$ ^" h) R
if(!StringUtils.isEmpty(html)){ * C" G; I! ~# W* K& H; P return html;! X& o; x2 c1 z/ Q; R
} 3 \% P1 }- f( _. S- q2 F //查询商品列表 / Y+ T, m) M' d; z/ |( ~ List<GoodsVo> goodsList = goodsService.listGoodsVo();& g4 ?7 ~# u1 X1 n" Z7 l; t
model.addAttribute("goodsList",goodsList);* a" z! X; @% g# W: D8 G
//return "goods_list"; * |, J5 f/ h8 R1 `# p0 ?* D % k. I3 V' |# a# D SpringWebContext ctx=new SpringWebContext(request,response,request.getServletContext()," D; n1 p& ~& |- ^( I, W
request.getLocale(),model.asMap(),applicationContext); / F o: w) R" V- Q //手动渲染模板并且存入redis,框架渲染的具体方式也是如此9 Z2 N+ y {: K
html=thymeleafViewResolver.getTemplateEngine().process("goods_list",ctx); 4 ^. n- s0 l/ E if(!StringUtils.isEmpty(html)){ ' k/ X. |' n; D* K Y redisService.set(GoodsKey.getGoodsList,"",html); * b7 P2 h5 [( ` Q2 |2 v }( n0 t N: Z. M5 A7 G
//结果返回 ' f# O4 m/ z1 O7 ~6 i return html;6 Q! x% M& g4 V
} 8 ~" i5 O7 W; ` m" x! d! Q4 \; X13 Q3 C1 t) s! m* j+ U
2 . A0 y: ?. k ^/ M3 f! L( f1 b. W3 1 W* O" E4 k* a; o% T( O7 M4/ Q z2 d9 Z( j, |' m& B( o
5 5 e0 j- E$ W, r- c4 C1 L6 # c& c# ~! Z$ ?# @7 , Q1 y% w4 }1 p( c6 F! O8& U1 y2 Q0 Z( L8 W- h% E
90 K- _0 R: u+ Q3 I/ e) w* X- }6 o
10 9 o, {+ V! x3 J) @3 }8 @117 ^; f" k3 ^) X9 B" {
12 0 |2 ~' w u8 p# `1 W130 a: F$ D2 @" a: [( ?$ E: U# w4 u0 A
14/ ~, d. d+ s8 \, h3 p- T4 ^
15; K7 p" h: Y0 E$ a( _+ I$ o
16 6 Q# L0 w# P4 b2 t3 [179 e$ T; b3 d7 w7 V7 ?% h) V/ x
18 " U; D, a3 C3 s6 K19* l( M* \1 m- I
20% X. g, a1 f" h) [" F& r
21: L8 ~& T( P' I+ W7 E% ?
22 . w2 g* c1 }# x1 d! Y3 @5 S: i' W& h g23 ( f" V8 C$ b2 P1 i4 ?3 \24 u4 j j% R$ `9 W' ]0 G' V
25' W+ R' t, S1 ` l' C/ o
269 `6 I) r7 A, w( K. d' R
27& D0 t( O0 I0 D- u7 E
28 ! J6 q5 |- k% ?- D; O& h298 i% R4 M$ z- d; a1 `3 W' _: o8 h+ i
30 ( x3 \4 S5 |: h* U7 P& d31 % [. ?& o- W+ a+ ?3 e. k32 4 q- K, ~6 }( v+ j7 }. L& w; ?2 `8 J& t
1 A8 x- V' p: ^, {
2、对象缓存 8 I4 ~7 Y9 {3 `* j: K4 q5 b & L4 E% _- I% T; Z, Q根据token对秒杀对象进行缓存,如果缓存中还有秒杀对象的信息,则不去数据库进行取出。 $ \1 @& g, f# m- i0 E+ c" Z 6 h9 S9 n0 Y$ W) c! `4 e6 R* F# L/ n% q& W2 q3 b
3、页面静态化 }' E( w* H) P0 L; r" m* x( Y* X) T+ J4 @% t4 N
页面静态化常用的技术为AngularJS、Vue.js等等,此处使用js简单模拟该过程,实现前后端分离。页面静态化,其实就是把用户经常访问的页面做成一个静态的页面,避免每次都要从数据库取出数据并且渲染到对应的模板文件中,从而减少服务器的压力。, y$ K' \$ s9 a' W) t1 o9 U M) s
页面静态化是将动态渲染生成的画面保存为html文件,放到静态文件服务器中。用户访问的是处理好的静态文件,而需要展示不同数据内容时,则可以在请求完静态化的页面后,在页面中向后端发送请求,获取属于用户的特殊数据。: }& b) ^( E/ `4 I9 @, D8 ~; Y1 n7 o2 h
页面静态化是一个以空间换时间的操作,虽然页面增多时会占用更多的空间,但提高了网站的访问速度,提升了用户的体验,还是非常值得的。8 l2 D; G4 g1 ~
关于原来的页面,正常的前端访问后端接口,获取html页面,展示,而页面静态化,是利用ajax异步传递数据,在前台进行数据的填充,实现了前后端分离。 $ o0 a/ d1 t Q) C+ S ; f2 {/ V6 X' r$ g6 ~, d@RequestMapping(value = "/to_detail/{goodsId}",produces = "text/html") {6 @: v0 s* G- _+ F7 c6 E @ResponseBody8 d+ G2 y w5 S
public String detail(HttpServletRequest request, HttpServletResponse response,$ a" s: A- s- n: T! M6 D2 N
Model model,KillsUser user,( N& U! v' O" B0 D; l5 T
@PathVariable("goodsId")long goodsId){ % b, t* J8 R [; E9 M9 | model.addAttribute("user",user); 0 L. z. J$ F7 u1 A8 ~% j# n# B6 U) `0 c: u" }
//取缓存,如果redis中有相应的缓存,直接取出使用$ l1 X$ r+ `; }, n6 y P. c
String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class); ; K2 V1 M4 t. L4 n if(!StringUtils.isEmpty(html)){ ' {7 `- i. b! f1 c return html; 8 O% I% W3 T6 @! [; k" d9 W }: t k: X: {, Z- G7 ?
( X+ F' K# z# Y1 {$ }# }3 p8 v( \
GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId); ' `! l7 j1 V. R5 W' l$ L model.addAttribute("goods",goods); 8 [$ h; }, F: g6 f& h% ]# b / v1 {" J' C4 J9 M long startAt=goods.getStartDate().getTime();6 _# B* Y8 o* _
long endAt=goods.getEndDate().getTime(); # ]& v2 E7 }% S4 a, C long now=System.currentTimeMillis(); 7 S. O; z/ e2 T" q) W$ E1 d6 B8 I6 ^) m2 @# [
int KillStatus=0;//表示还未秒杀1 W& ?5 P% }2 o5 h& V. Z
int remainSeconds=0; % E8 C1 [2 A' _) ?. j7 C2 V- w1 U% p! Y) D8 F
if(now < startAt){//秒杀未开始+ ~& D" I+ O! w& p1 Z
KillStatus=0;6 A1 p7 n; s0 ^3 }
remainSeconds=(int)((startAt-now)/1000); 7 u& U( V. s% R" e# D1 b }else if(now > endAt){//秒杀已结束 U1 a" i" _. j4 d6 {( O
KillStatus=2; 5 H$ c/ f$ S6 \! P6 ? remainSeconds=-1; 8 z5 l9 L2 C, @ }else{//秒杀进行中 # L! I' R: b/ S6 e# a KillStatus=1;* {" p/ b" i/ N% T. Q4 L
remainSeconds=0; 8 S1 F& W4 m* g% i7 F; m7 d4 V) | }" Q& n8 l0 J) z
model.addAttribute("KillStatus",KillStatus);8 a; y# d$ g1 i, c2 W
model.addAttribute("remainSeconds",remainSeconds);" }0 V# E; ^4 J4 t
/ C( ]5 J4 g; `& P% {( q0 x 3 c* W. R: s& B& Y$ B# I P3 ~9 ^- R SpringWebContext ctx=new SpringWebContext(request,response,request.getServletContext(), - h4 D. h. h* @) _ request.getLocale(),model.asMap(),applicationContext); $ F4 {' w a; G G! d0 V, x //手动渲染模板并且存入redis,框架渲染的具体方式也是如此/ O$ }( V1 X4 j' K* N$ e
html=thymeleafViewResolver.getTemplateEngine().process("goods_detail",ctx); " h+ ]- c' A# W2 |' |0 E- T if(!StringUtils.isEmpty(html)){6 ? [" _# q n. a \; Y
redisService.set(GoodsKey.getGoodsDetail,""+goodsId,html); $ n& ~( A3 V* x }7 n3 L/ t% A3 p7 O) O* R' ?
//结果返回. @" f9 B) V/ o9 h7 v3 P% Y$ G
return html; . s( g+ x# w" ~4 w8 P" e$ B //return "goods_detail";! q9 I. v, F# b, O8 F" o0 W) ^
} - U4 J1 L2 x* L. W1! T( _ u: E7 o5 W$ H4 E
21 r6 Z3 m" G# A; G6 Z2 @
3 5 M3 `! g( i" ]* D8 t4 . [# J1 _7 D& g# G( m5" U" p; U2 W! g4 ^& A( i
6' q5 n, C, a. `1 o! H: [
78 B+ L) A& V$ _
8 7 w K4 ?7 r0 d8 E2 {, j* T9+ I9 N2 f( e( i$ F; T1 n# r, l
104 E$ D" T# {* G5 n
113 h1 L3 h; t, [$ U
12 2 w$ {' n3 [6 a8 T& l13 % Z0 c/ N7 V2 _14. S6 H6 Z' {; B
15 ! E4 l4 F4 q" y1 v# `1 b C16 : X1 A% n- b5 ^. W$ }3 V179 X' @' @6 Z2 m$ A% o( S( n
183 t( m$ b( i$ r, F# \7 D- t( c! F
19 + |1 m2 {& e' X, |1 V( I4 y20 ' u/ j/ }# C6 c4 n: {+ y211 m$ t. p+ q' _- U. ?% P) A
22: o/ C6 h" }+ P8 a
23 5 W- e3 V" c9 A. L0 h; f" ], f5 Y24, e3 V0 ^$ Y# `. _! U+ P; W/ Z
25 0 r; H+ z' o. T# ~6 s2 \5 u' n8 A26 + N% { V3 s' \276 ^ {$ C2 c& O6 k) r9 S0 O
284 z9 c3 K+ I9 W7 \0 K5 d: n
29 ' ~$ D5 | G- U z30* X+ q" X3 w3 u8 {
317 h5 |1 p. A4 c4 k5 Q6 i9 G& N
32 4 e7 V/ r) R" d3 N333 z0 C9 E G/ w3 C
34+ m$ V9 R/ Y( [9 q% N
35& f* Q2 P+ Q2 `( m/ i
363 e+ |$ @1 A& E6 [1 S/ @" C
37 % a( `4 R( x2 l' E) T, J1 u38 - ? B+ P k! J$ Q39 7 B; }6 T4 H7 ]/ E; K% t40, V/ O+ R# F1 r2 H1 ?2 q
41 ) V% f- M! Y5 v4 s- d& M42 7 {5 i% q5 W" ~ K43! |6 p" l& P" ^- a
44 + s: l5 J& k/ T7 \& t45+ e2 j8 W; E7 F- l
46 U& ^% H( T' N; N( I47+ i/ u$ J4 n/ X) f
483 _( i. @. `9 O9 C3 y [
前端代码通过ajax异步请求后端的接口返回数据,并在前台取出数据对页面进行填充。5 ?3 E# Y+ {- H, c G7 {6 M
, [* x) T. d7 G8 J//前端/ O6 I. @! \* }
$(function () {: U8 D! A4 C5 K+ j
//countDown();( [3 g# T P2 l4 u# s9 ~9 Z9 @
getDetail(); " }- B; }& x! _0 s) b+ W}); 2 p5 R* z5 Y+ K/ Y4 _/ hfunction getDetail(){ : Y! Z; G5 H9 y8 B var goodsId=g_getQueryString("goodsId");: \8 m/ J& E0 ~3 H, U
$.ajax({" a$ l0 s L6 h# ^% Z6 c0 `8 `
url:"/goods/detail2/"+goodsId, . d9 \( i7 y% W9 } f+ c7 c5 Q type:"GET", 7 R1 [% _' }( @* C5 Q) O; L3 H success:function(data){ * O, T1 f2 ]1 W! p! j if(data.code==0){ ! i8 ^( z% [: } render(data.data);& |6 N5 T( o& \+ S1 _3 P1 Q
}else{7 ?8 j5 V8 Z! b/ \9 B4 h
layer.msg(data.msg); 2 A) m, G) ^1 U- W! D6 N6 Z; }* E } : Z6 u2 M6 |% s( G }, 2 S' g2 Q' Y: S8 O+ @9 H) g error:function(){5 l6 n/ l* S2 A- N/ X1 h* `( M
layer.msg("客户端请求有误")' i) \9 T! \4 z# t
}& t7 q7 S8 _4 J8 m
# K" h- o) t$ F1 V6 L8 j: } });! z+ S T: q" Y0 w. Z; L
}3 b. V! g7 L; n4 \
/*text() :设置或返回匹配元素的内容 * V+ I6 d2 v$ r8 l+ [& g7 N2 V3 Z: n* val() :设置或返回匹配元素的值 2 G! ?9 R! r8 V y. r* G* */, r4 y5 s; R# I3 Z" C
function render(detail){ ; c' @; q! z, T U9 z- E var goods=detail.goods; {2 _: c' S% N3 N7 w
var KillStatus=detail.KillStatus;; u) b! O! X# W) y
var remainSeconds=detail.remainSeconds; , F2 ]4 l# y4 r" s9 o9 o9 h" E* x9 M var user=detail.user; t" {6 V$ z1 Y) z" A8 Z- @8 R
if(user){8 O3 e, v$ h i# F# l& z" Y
$("#userTip").hide();& p. g% N9 t4 F
}: [+ [1 V' [. W. a. V4 [
$("#goodsName").text(goods.goodsName); + E% p- N& Y5 q% t $("#goodsImg").attr("src",goods.goodsImg);5 N$ X! X$ H, G& s
$("#startDate").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss")) 4 h, C$ C; o7 v4 l+ b5 { $("#remainSeconds").val(remainSeconds); 6 B1 W7 B+ Y( J3 D/ h; |+ g8 z0 s $("#goodsId").val(goods.id); 7 D3 J" l* E0 k$ q6 a $("#goodsPrice").text(goods.goodsPrice); ) r* A- Q. Y/ ]1 D $("#miaoshaPrice").text(goods.miaoshaPrice); 8 W3 T# ? {8 c2 X# j $("#stockCount").text(goods.stockCount);( D1 j% w, _) e! R3 b7 P
countDown();" ~6 U" U# Q# t/ j0 i
} 3 P$ ~- }% `. m2 V! a' M//后台 ! J$ h% g( ^5 g3 g6 d% Z@RequestMapping(value = "/detail2/{goodsId}")6 D J* q$ X3 Z6 ^2 j
@ResponseBody 6 i, U2 a n; q! L' {( _ public Result<GoodsdetailVo> detail2(HttpServletRequest request, HttpServletResponse response, " `2 q/ j% r, F- m l7 d Model model, KillsUser user, ; p' i5 i* @6 Z2 [0 |5 Y7 k% ^( Q @PathVariable("goodsId")long goodsId){( \- {0 {% D' F9 V/ s" }
GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId);/ D- _1 ?8 z! y3 A3 c
z0 b( x# Y" w9 g7 y. A long startAt=goods.getStartDate().getTime();' ?$ h" J; M, g/ _9 @
long endAt=goods.getEndDate().getTime();6 z8 J" e; \$ O6 }$ ?) A
long now=System.currentTimeMillis();3 e a0 E$ V# W) U$ s1 p
7 L2 M9 `; J. R/ M% w, u int KillStatus=0;//表示还未秒杀/ ~! G3 C7 v4 n( W
int remainSeconds=0;8 r9 \4 j$ n& k, ~( o+ v3 Y" j
% A7 @ r" a; }0 d; D4 S9 k if(now < startAt){//秒杀未开始1 ~4 o6 A" e+ a
KillStatus=0; % t9 H5 D4 F# X) |: l# P/ Q! z remainSeconds=(int)((startAt-now)/1000); . E$ j& x2 j) [) }2 K ^ }else if(now > endAt){//秒杀已结束 ! U, }2 x% K( C; r KillStatus=2; % n' p9 _% u* S2 w- Q" u+ p' K U remainSeconds=-1; 1 ?( ]3 e$ A9 b( c+ o }else{//秒杀进行中2 T( \1 C# `# d0 G
KillStatus=1; : @3 F# c! P Y# D* Y# s remainSeconds=0;3 N6 z" o* t" V0 o8 C0 a
} 4 Q' n: s, e9 F7 G) d . d: I- T$ {: @+ x; V GoodsdetailVo vo=new GoodsdetailVo();# J$ Y. C; E- o/ L3 E! Q# {
vo.setGoods(goods); ( A( Z/ G5 R7 |( F. C3 o* M, f& `0 \2 v vo.setKillStatus(KillStatus);1 k; W# C g) F& C1 f0 P
vo.setRemainSeconds(remainSeconds);1 `, u' Q( T- l5 a. c' \
vo.setUser(user);3 w4 e- a6 H# x9 I$ l
return Result.success(vo); " z' ]5 X. x4 X+ B, x, A }* O' Z5 ~5 A2 C i8 V( v/ B
1! w8 K" e# B/ U: G# Q5 n$ m0 o/ ^
27 a; n g1 |$ e2 L
3 ! u0 B( F( n8 u* K0 [4 * F1 s5 W+ M4 d K5 3 K" e' g$ O3 j6 N" S6 ]; Q( E) f: n/ G$ ]. d
7 - W0 @: |& U$ {' }; ?8 v8 0 V4 E* u6 L/ H) I% ~* `9 4 n3 U2 |! P: y5 |# i10 6 M" B0 R/ U9 n$ ]11( X. m9 j5 X, n/ v: M2 e) \
12: ~$ Y2 P- j! m; ^
13; ]+ A2 s+ m! v' v
14 ) p3 V$ c& X- q( q3 k+ f6 |1 \15' w) }, c j+ n+ u/ W# w. b: G3 G
16- m8 d, Q6 f# d1 M2 C
17, Z3 h. q7 _" s; S2 W9 [0 ]3 Z. Z
18: q- k8 C7 [7 z' K
19 3 [) z3 p6 n8 J. T203 I. v0 ^$ r% W
21 8 d8 G. _! t2 U223 \) e0 F6 U4 x4 P
23) ~' p6 g3 y+ e* f ]* G2 m
24 2 `6 ^! V8 G& Y% }+ p9 p25& t$ x% g5 d* t
26 - b& K9 M& E0 C. U/ k27 & v) n h' |- l5 V+ D( e28- N4 W6 p; {4 P
29% y7 ]5 d2 r; {
30 2 K* K0 ~0 c7 T31 ; R+ w# u: u8 B7 C327 B$ X6 e+ r. B y; \; K/ c
33 # ]" E$ j5 n. J, Z+ O34! y6 s7 M+ `) a. ]0 o
35 * D0 J- h3 k5 D% R36 3 O; k% T8 O2 w- g* p, t37: b. H6 G) L- b2 ^% T
38) M: J" d; x! y! F+ b- y+ n
39 * n2 x8 \3 N7 k8 e405 E. }( E$ [, E4 C) }
41 0 s9 d, J7 W- P; J R; V' g. N42 9 T2 |$ m2 `6 R9 H0 W$ F7 J, @435 r! s% ^; R2 `4 X% O" h4 \
44 . l% r" e k) m45) i: `0 a/ }- m- l
46( v4 p9 H& ?0 l! T! B0 {
47& {* B9 M* G3 O% s6 x, ^7 g1 o! p
48 / E0 M- u' q b1 ]495 e5 v5 M/ h2 v1 ]
50 ! R: G g2 ?3 Y1 J) h' N51 * q! d$ C' u3 D( S52 : ^: L2 a4 i# }9 [% e, P53 7 Q- U* v. p+ J9 {" o0 r54- X* Y. b7 ~4 `; S! m
559 ]. V* M+ H% E. K# E% _7 c6 t, [
56 + R V7 M5 B3 M0 {: S+ N0 B" |& ^571 N1 K4 ?' c3 f! E0 C: O) Z1 L8 t
58% K0 }( D0 e6 e) \
59: m3 N4 {6 h/ t3 E, r8 @
601 r7 L8 O% `/ m( r& ]
61 / b% v6 O. ?5 g' f5 b! W3 v4 z: \62- D3 L! L+ G' `% _) Q% y, T1 ~
63 " p; V8 ^3 l4 D6 }64 " j+ B s+ }1 G65 & W: |8 H# C6 A' Y66 ) _/ Y; U4 I( t# K" }67 + L# q" g: M3 P# s/ b/ T68 2 K' g& Q: `& a2 n& r: x69 9 A8 B4 T w8 v9 A2 g! G70) x% a3 t9 x; d
71) d7 \8 |& H. n* i
72 1 ~# \, k! x: k5 E6 c% x73 : D4 e' E1 \- s, [746 E) f7 o, x! k. {. g `
75" f, i2 _; P8 V s( H
768 r$ M( q( M: k9 S' J: h. s7 \3 L& M: e: j, u
77 + J0 P! D0 z" l5 v四、接口优化 , z# I o5 d# \) \- s) V) v 0 ]' i! t/ O) `, j* v2 N) b接下来是对于秒杀接口的优化,这是最初始的秒杀接口: 5 Y; K; V0 ^$ K7 J0 y刚开始的秒杀接口的业务流程是这样的:判断用户状态->获取商品库存并判断->获取秒杀订单并判断->减库存。这个过程需要调用数据库共3次。 * w) R8 h3 q$ b. ` ^秒杀需要面对一个很严峻的问题:如何防止超卖或者少卖?此项目中总共用了两种方式,第一种是在减库存的sql语句中加上对库存的判断,来防止库存变成负数;第二种是对数据库中的秒杀订单表中的用户id列和商品id列加上唯一索引,防止用户重复购买。2 h1 q# F" |% `0 `6 x/ \
$ H# C& v- c+ I; w0 U3 T0 t. d' a$ X
@RequestMapping("/do_miaosha") E8 f' `# G1 R% F3 ]+ O5 f public String miaosha(Model model, KillsUser user,9 w# s5 Z4 a I t3 x6 w
@RequestParam("goodsId")long goodsId){ ' M6 ^) F3 A8 B, B% W model.addAttribute("user",user);, D2 z6 J& u2 ~" U* m
if(user==null){ & {. P+ I0 \& ?2 ?$ e# F return "login"; * ~0 W! i- p+ N5 H% @ M$ ] }; \7 I `: V7 K' B3 b
GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);6 M+ ~" S( l j X
int stockCount = goodsVo.getStockCount(); 6 T* V4 D% G& Q //库存不足( c5 p, M4 Q2 n
if(stockCount<=0){: X0 ~8 h( O# \5 P4 c( A
model.addAttribute("errormsg", CodeMsg.MIAO_SHA_OVER.getMsg()); 3 S- d# x+ E7 r" U! Q return "miaosha_fail"; 2 N7 ~4 F; D, T1 g' ^, w2 Q } ! K, ~( l% t8 I' o //判断是否已经秒杀到了1 p. ]! n; ^3 d4 n- V
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);8 d& a: d+ ~, f( u1 F
if(order!=null){5 n5 F& [9 z& f K7 g
model.addAttribute("errormsg",CodeMsg.REPEATE_MIAOSHA.getMsg()); 8 m+ j1 W1 w, V7 z* ]$ p7 F# v8 U L return "miaosha_fail";6 e A) G4 c3 j7 u6 D; m" `% c6 [
} + ` m3 M8 J* c: H D6 h: c' K //减库存,下单,写入秒杀订单 a4 G3 W4 L- k! N5 ?+ ?1 @8 ?
OrderInfo orderInfo = miaoshaService.miaosha(user, goodsVo);0 Q# c2 x% M' J
model.addAttribute("orderInfo",orderInfo);. U6 C2 u+ _3 u/ `
model.addAttribute("goods",goodsVo);) U; F' p1 M7 A
return "order_detail";: F7 K& z" i* p7 D& E, D: ]
} , k' D; l9 G( k9 ?" w# }% X' h' }# s1 $ ~$ q$ f( |5 o5 I8 w22 m1 A! E+ z& W2 S3 v
3/ _1 D9 u8 q5 U' M7 e: [. ~, Q6 v, G
4 / W$ p6 W+ |! ]- z# T5& f* ~7 M& I% O* B/ i2 u# m
6 , X ~' Y1 ?0 K$ g* M" O% F# |7 ' @ i# {; @6 e2 {/ b0 _4 P8 - b! x$ t' z; ^3 u' A97 T8 M9 z* ~3 O3 j' `
10 2 m% { w1 E, y9 z1 j7 M11 ; c& J. h3 C! n12% d, Y: H: D2 v. E2 b
13 + ^8 u' J7 W' {) ^14 1 J& }& P( e- b( \- R$ p8 J2 C15 }. M7 v6 J9 l2 h$ a3 h
16; M! X% x1 s0 J- Z% s+ m
179 z2 i2 h5 Y C$ d% R
189 _! C( `+ c+ `5 U& C- V7 @/ X ]
19 ) `4 r7 Y+ o! m: A! D# s4 W200 @: [8 N$ O# y3 \% }6 Z
21 3 A# t: z4 }) c8 l. |/ M22$ h, f h, F" o$ l$ N+ T, A
238 Y, i; f9 z4 A' x4 W
246 Z* b1 e5 f# n
25 5 b5 y2 x8 N- n: R26 z) s8 q) n/ U: X* j/ O; a/ e优化:秒杀静态化,通过ajax异步请求调用秒杀接口并进行轮询查看是否成功,redis预减库存减少对数据库的访问,内存标记减少redis访问,RabbitMQ队列缓冲,异步下单,增强用户体验,同时减少服务器压力。首先是使用redis预减库存,我们使用一个HashMap来存储某商品是否售空,这里用到一个方法afterPropertiesSet(),该方法会在Bean的所有属性初始化完成后调用,在这个方法中,我们从数据库中查出商品列表,将每个商品的库存存入redis中,并将库存不为0的商品在HashMap中设置为false,意为未售空。(此处应加上对商品库存是否大于0的判断) ! N! ^9 k6 o' X' c在秒杀方法中,我们首先获取mao中的布尔值来判断是否售空决定是否进行下一步操作。使用redis的decr()来预减库存,减完判断是否小于0,如果小于0就设置map中的值为true,并且返回,否则进行下一步操作。 + b5 I! z! s6 _1 K关于RabbitMQ的操作,我个人感觉还是有点迷的,毕竟真正的秒杀不会让你一直等待吧?(没事,咱也没秒杀到过东西,问题应该不是很大,也可能等待的时间很短人们感受不出来?这里请懂的朋友评论区指导指导呗) 7 `( V2 Q, E& ]4 k. {此项目中使用RabbitMQ将秒杀请求入队,前端进行轮询确认是否下单成功来实现异步下单的操作。关于RabbitMQ,RabbitMQ是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,或者简单的将作业排队以便让分布式服务器进行处理。 - l! q3 L6 B- n5 c4 i& U5 ZRabbitMQ有四种交换机模式,分别是Direct模式、Topic模式、Fanout模式和Headers模式。Direct模式需要将一个队列绑定到交换机上,要求该信息与一个特定的路由键完全匹配;Topic模式是将路由键和某模式进行匹配,队列需要绑定到一个模式上;Fanout模式则是不处理路由键,只需要将队列绑定到交换机上,一个发送到该交换机的信息都会被转发到与该交换机绑定的所有队列上,类似于子网传播;Headers则是以key-value形式来匹配。5 |$ f# {# L: I
队列和交换机绑定使用的是BindingBuilder.bind(队列).to(交换机) 3 u, u' M2 f! o5 @+ P3 J& H) Z4 U) j 7 Y5 l' x! \ z注意此时秒杀操作并不在秒杀功能中处理,而是在RabbitMQ中的receive()方法中处理(见第三段代码) S% g3 e) C9 s
. z4 m1 u/ D& G1 o //用于保存商品的id和是否售空9 n6 O& g. l2 l8 N# _
private HashMap<Long, Boolean> localOverMap = new HashMap<Long, Boolean>(); ! k9 Y0 _) R# c9 h 5 L+ h1 H' e) a G
/** : @6 B g9 s( I, @# h, p* P4 Q * 系统初始化 $ H$ e) t8 j3 k, ] * */8 c' V1 |$ [4 C8 o3 ?
public void afterPropertiesSet() throws Exception {& r5 Q, z6 K; j2 z$ T0 j6 V
List<GoodsVo> goodsList = goodsService.listGoodsVo();) g. t9 N* Y( q, n
if(goodsList == null) {# Y# T5 [+ j7 k$ i! t
return;+ W6 y, i, b0 a7 `1 s$ i0 P$ ?
} 5 A* n6 B1 p- `' m- e+ g/ F# d for(GoodsVo goods : goodsList) { ! {$ t1 _" N0 b1 s redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount()); - \$ G7 `: u. ]' P5 x) x1 E& z2 ` localOverMap.put(goods.getId(), false);" l0 Q3 w- x6 v% H9 C
}: z( [0 k( K% g0 z% F* _
} & ?2 P$ c7 l) c7 J( V& t0 ]" G( a2 b; s
, Z- T6 e/ u1 j @RequestMapping(value="/do_miaosha", method=RequestMethod.POST) ; o# Y% P* C- {2 f* P @ResponseBody 1 ]$ Y& \1 B- W n) S5 ] public Result<Integer> miaosha(Model model,MiaoshaUser user, ) J" x6 O1 }3 w; a" \8 \! `' ] @RequestParam("goodsId")long goodsId) { T; E9 P; k9 H. u; { model.addAttribute("user", user); , r2 I5 v7 _$ g, f, u& \2 @" b if(user == null) { . C+ I. W1 h. T& G2 b9 A return Result.error(CodeMsg.SESSION_ERROR); `7 g. c2 k" A } I9 D9 X# R: L8 ~
//内存标记,减少redis访问8 p' [' D! L2 F& o, m# U; f2 L, a
boolean over = localOverMap.get(goodsId); / p( W/ s: T& F% V if(over) { 7 Y+ m: q' z) l return Result.error(CodeMsg.MIAO_SHA_OVER); * j# J, N) b- ]" q) w$ e. f } % }0 }3 `5 {+ Z; `, V$ y& j- R: V //预减库存' V6 j' h! t7 F1 B& N" p
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10 $ c" }% Y* x: g% H if(stock < 0) { * y% U' \# Z4 V6 x localOverMap.put(goodsId, true);+ G0 T8 c7 ?( w: p
return Result.error(CodeMsg.MIAO_SHA_OVER); `4 z% t- o' y2 j4 e; R
} # d4 [! c, }: D3 d //判断是否已经秒杀到了 9 ^( p; C6 J; ]1 L' U' H+ L MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);, ]" ~1 H3 u5 B4 O. I
if(order != null) {) k& j9 ^- J5 D$ J/ y% k
return Result.error(CodeMsg.REPEATE_MIAOSHA); ; y Y3 o" W- L& w B } * v/ k$ Z$ w# Q/ y8 ? //入队 0 Y1 }3 ~, Y. h1 A4 v( l: f MiaoshaMessage mm = new MiaoshaMessage();" Z7 o [& i, W2 r/ f
mm.setUser(user);. B6 `" j6 A: G) ]
mm.setGoodsId(goodsId);1 |: l' G/ S9 S
sender.sendMiaoshaMessage(mm);6 }( D: T. h Z
return Result.success(0);//排队中 0 \$ t4 u7 G$ |& H, H }( U0 R8 k' y1 u7 }! F5 R
( g0 Q+ g5 m7 v- C" U4 c
/**MQReceiver类中的方法 + S/ c/ b3 b7 q4 h! X# }) E- V * 此时到达这里的请求大大减少; R! ^2 P7 ]. K2 A
* 因为有redis库存的拦截: x2 a l! F' T% c% P
* @param message. J; p) w6 b1 u$ @, z' V7 e& ^ I
*/ 0 A+ a8 v( U* h% ], [$ N @RabbitListener(queues = MQConfig.MIAOSHA_QUEUE) 5 {$ y" P9 p2 ~8 u& g* @ public void receive(String message){6 F" [" ~% m' P8 z
log.info("receive message:"+message);0 P; F8 G- e; G5 |3 D
MiaoshaMessage mm = redisService.stringToBean(message, MiaoshaMessage.class);# {! X5 |/ p7 \8 _' E3 a, b$ m, l
KillsUser user = mm.getUser(); # ?0 e4 D" e+ F long goodsId = mm.getGoodsId(); ( F8 g6 ~ O/ F //判断库存% W/ u5 Y" E+ t0 \6 `. q
GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId); : B+ p) R, e. B2 D9 [* E( @8 e5 J int stockCount = goodsVo.getStockCount(); 5 d, j) S) |( D9 Q' Y //库存不足" P+ T8 I/ T& x) V; ]* f. h
if(stockCount<=0){ . ^( c i' F) T return; 6 _* _. e1 y5 @$ ^ } 2 v' T+ p$ O% @ //判断是否已经秒杀到了,这里可以不去数据库查找是否有订单0 S1 x# f3 @) n) ^
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);4 E9 r! \, W ?5 ]6 X# B
if(order!=null){8 a ]8 @/ M* j/ ~6 C
return; 4 ]2 X' ~# x2 a e8 |2 G* p& u }: l+ n7 f* i& d: E j: H M
//减库存,下单,写入秒杀订单 5 F8 U6 m% @3 Y- Y/ ?: _0 @ OrderInfo orderInfo = miaoshaService.miaosha(user, goodsVo); + a# b8 K# U0 B) ?, b, p& f }% }$ l2 N }7 n& u" p/ h
1 4 {2 E+ D# ^; L. x! {) k2! {! y% m: |- E4 U0 m" _ i [8 \
31 C/ B2 B$ y) W# Z) x/ Q* {. h" u
4 # v3 n+ x8 l4 n. D% L$ f' y5 ; Q: u6 K: u4 R0 \3 c6: v3 G/ w/ b. w4 f3 ~' P
70 s% a9 ?4 x% r
83 |& F. ?/ D* s
9& W) F( @4 q* s# I
10 1 f( d6 r$ }- @9 u' c# ?8 L- M11& T3 s% x q2 B- R
12. \% L. R; A4 |' a
13 0 E7 f2 p+ Z' m+ K" Q% m14 5 X( z( h& ~* H7 \157 E2 R ^8 _: D! i, u) R# M: L) ?
16 0 o) j$ n3 u. J( @4 g5 b17 % Y4 m6 A, x- @! l* A; M18 2 ^6 X( [- E- t* u3 h: v, t19' ~. V6 A! L7 B$ P: t& g A
207 _( C7 C3 J( N* _ Y3 p
218 U) m1 z7 O; L/ _. y
224 @6 G+ M* }: Y9 t: X, F
23 ' N) l( d8 [- n1 U6 B+ w9 F24 % ]! c, C0 A: e- M( S7 e" P25 + \0 b- ~+ S1 w. L4 {8 D! Q26 $ m* e; a; l P8 Q27 3 J; e, H( P$ C) t% y8 w) a28. k' s# v8 G i" ]0 F( V
294 k+ A; `/ }" u% v1 [4 l( a" ?
30# w. g! \$ j" o# t/ r
318 @6 x" M ], R& U. D
32 , i( n/ [! o; D. W33 0 |6 _; E7 G. L4 T7 N34 ) t) Q7 M( F4 ]& @- b. P* |35( R1 r2 t4 ^: i/ E! a4 T- z ?( w
36 ! a# f4 n8 ~* Z: s3 n37 . S6 Q& q! n6 p/ R' G& X5 C4 R383 ~1 O9 {. Y- ^. m+ i9 @: u
39; A3 n1 S! L" `5 B( k/ Y0 R: ^
40! _" a' k6 e% x1 e% S4 ]. z
41 @ A+ R; y* _$ _7 F# i/ y5 D; X42 " G5 z( b- _% @( O3 X! n+ x/ ]- ^% ^434 h8 _* j! V, |% i% j9 A6 R
448 W; b9 Z8 C- F9 C4 W- o3 k9 `1 y( Z4 i8 @
454 h, \. I+ X+ d
46% a- c+ |: \7 l' B
47 - Q# N5 Y; u$ y8 g6 Q' D& Z486 |% h! R2 t) b8 s) T
49 5 w2 T% h& C4 o% q+ k* ^50 _6 |0 z# [1 R" z7 i
51 + X. d) K: q# @* a E' `# B, ~4 L52 $ u/ F/ Z! h# `) a" r0 m531 h& D) }( w* J2 s- J; o
54# q( F5 \- h W
55+ L. [$ u" h% @# T3 }5 n
56 4 _5 C, u7 S- n# _/ B% i3 t! S P57 6 ^5 [" |& u7 [) D# l: F58 / u- U, I4 ~: G, ]! x59 ) ]/ A- x$ k% M' y: g$ e* H60 ' g' G' v; Q0 ?, w; X; d61$ c# @" w+ n5 }* V- r# N9 G
62 * k. A' G: R `$ `5 n6 I5 u632 ]. ?8 r) y2 O- X, l: @
64 4 ~9 t- ?/ w" C, f; Z" q65 $ f4 @& B) U7 j( d; H66 ' ]2 i* H, p6 F$ v' r67 " \) G& u6 m' q* i6 d- e! z68 . V6 k2 F6 C; `. A: A% m7 |69 0 Y# {8 P/ A1 i: Z3 b/ u70 , A8 E7 Y' B8 D, q( O: N8 r71- g8 `8 g7 e; n6 A4 F
721 S" G6 I1 F v: _7 Z
73 [* n8 e; u5 Y& L8 g5 K* A5 N7 m74( Y+ x! p: l5 u; M
75% Y$ v) U+ t4 E3 b4 z& O# ?
769 |# z4 j; a- L; m# v7 }
五、安全优化1 @; c1 w. A- G0 a, Z
5 h9 V+ y0 k; m% y( E
1、秒杀接口地址隐藏 2 r+ Z# q6 p# h" G" l! _' I) @1 ?2 `' ?7 q/ {( V7 G
在秒杀真正开始之前,我们的秒杀地址不应该是可以访问的,这样会造成极大的不公平,如果你获得秒杀地址就可以在秒杀开始之前买到商品。因此我们可以在地址中间加上一个随机的path,可以利用随机生成随机码,并把用户id和商品id作为key,把path存入redis中,在真正秒杀之前会再度把传入的path和redis中的path作比较。( ^" K, q g4 k9 E0 X" O' l$ T/ W
5 v! U9 \8 w$ a& j0 Y) A3 C9 |
2、数学公式验证码 ' ?2 S) e: E# N* x9 S Q, q, M! u' M! l- ~为了减少真正进入秒杀接口的用户,在商品详情页面进行一个验证码的输入,对算式进行计算并把值存进redis,在秒杀方面之前对用户 输入的验证码和redis中的值进行比较,从而判断是否进入秒杀。2 b9 f. c# z3 ~, t' s6 n; g0 a& \
1 W6 M9 X M8 a; c A4 ?- r2 z3、接口防刷 # W9 c6 w0 V4 @3 R# |: q& x' B+ v! _2 |7 Q% o2 {$ U+ P: N
接口防刷是防止用户短时间内多次访问同一页面,可以避免某些脚本的攻击。原理是使用redis缓存,假如在k秒内访问超过5次就拦截,key值是固定前缀加上用户id,有效时间是k秒,而value则是从1开始,在不超过5次的前提下,调用redis的incr方法。如果超过限制,就拒绝访问,在拦截器的preHandle()方法中返回false,拒绝访问。 {' g, {5 J: U- X
但是可能会有很多的接口需要限制,所以会出现冗余代码很多的情况,并且这样也会使得接口方法变得十分复杂,代码可读性也会降低。因此我们可以声明一个相关的注解,如下: * c1 ?! B2 V) }" l% A0 D ; r. V u1 G6 S: }- x , z! Y5 {$ D2 y@Override: Q. y. I& e* V/ @- e5 d1 w
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler)& m/ C! {2 \/ N" [6 F" W1 T& a
throws Exception{ 8 T$ c: q, o: L! `& a if(handler instanceof HandlerMethod){7 P. O; u' N: p' Z" C$ i
KillsUser user=getUser(request,response);% G% _, y: `. H9 E0 k, [
//将user对象存入ThreadLocal中8 R- _' R( z; B
UserContext.setUser(user); 3 P* K$ F5 j; @6 C2 n+ a. o# n
HandlerMethod hm=(HandlerMethod)handler; " s0 \+ |, F1 y% i AccessLimit accessLimit=hm.getMethodAnnotation(AccessLimit.class);1 |4 Q$ R/ X0 [3 P' N$ ?
if(accessLimit==null){ 3 u: I+ o/ _) o8 V! I, y //说明没有这个注解 跳过( H7 M/ U& y8 \5 d4 L( g* Y' k: x
return true; 2 Y1 _4 ^% `% V5 C1 q } + p1 N& F- ~4 j1 c$ p( u" |+ D/ e0 ^, \: j& z6 \0 J' p$ k
int seconds=accessLimit.seconds();% v, u! P- P5 Z1 F U
int maxCount=accessLimit.maxCount();* Y0 s6 g! A6 b2 Y6 H- z& ^
boolean needLogin=accessLimit.needLogin(); 0 D- V9 Q& \+ m) F# q" X" |0 w String key=request.getRequestURI();+ Q9 v) G) `' r" S5 K- D
//这里判断获取的user是否为null cookie决定 + X1 t6 V1 t+ Z" K; A; T( X$ ^& X1 E. u: q if(needLogin){ / X7 V+ c* a+ n( ?/ G; m if(user==null){ 9 [: w2 Y, E/ O" e+ j, G render(response, CodeMsg.SESSION_ERROR); # H' a: n/ h* q( R# h0 y0 m0 ] return false;7 c) n3 `: k7 [. {/ f7 D+ J, t# f
}7 N$ Z) _4 H+ n h% S6 m
}else{ " F3 Q6 R; o0 D+ e$ f& }+ f, Q //do nothing, ?! o2 n$ m) | Z
}, Z$ s! l: ~1 n( h, C
key+="_"+user.getId();) s2 f$ o5 t$ Q; ~5 S& Q* A( c
AccessKey ak=AccessKey.withExpire(seconds); * _% x4 O( [" i& i5 O, Z( ` Integer count=redisService.get(ak,key,Integer.class); % b' b4 \: _6 d; |$ ` if(count==null){8 y2 H: v3 J" J$ X5 o$ p
redisService.set(ak,key,1);) A9 N* q2 b1 B6 J
}else if(count<maxCount){ - ^7 I, B2 @2 v6 n3 x7 G C/ Y redisService.incr(ak,key);1 r" I# O" n( C6 y
}else{//短时间内频繁访问& y! o1 {& Z$ F# `7 f: p2 L b4 ]
render(response,CodeMsg.FREQUENCY_ACCESS);0 H% }) }: i$ y8 L) f
return false; " _- R. @! ^$ H+ H+ { } - L4 f0 A, z. ]% I/ p/ ? } z* T2 ]1 _6 n& V# M
return true; ; `3 W: h8 ^" @* g9 D } . E/ D. d% T6 R, {, B% x* b" y. k& k
4、用户存储 + U# ]/ p2 C9 @3 U3 E) w9 ]2 R( S" ~2 f
在多个方法中都需要使用秒杀用户作为参数,判断用户是否为空而决定需不需要跳转到登录页面。因此每次都要从数据库中查找相应的对象来返回。我们可以使用一个ThreadLocal来对User对象进行保存,而ThreadLocal的有效期就是在当前线程内,所以在用户访问的过程中都可以从本地拿到一个秒杀用户对象而不用访问数据库。 . @4 l: m3 r+ C* y( i0 H/ h: I$ f; Q' U2 C. x! F" O
public class UserContext {& j* @% l7 T5 |1 l' v& G( _. B8 B* s
//将user存到当前线程中 ( p5 d4 k: _6 e4 J% d# w2 ] private static ThreadLocal<KillsUser> userHolder=new ThreadLocal<>();3 X" ~( e. Z1 [4 u. N