高并发秒杀系统方案的优化 1 o2 w7 B6 O/ v; m/ u& q& m9 b 9 e( A) z' N* k ~最近接触了一个关于高并发秒杀的项目,在这里稍微整理一下关于这个项目的一些值得记录的一些点,以下是源码地址:github 2 M) A8 c8 S `6 L: n& }8 `# `高并发项目的瓶颈主要在于数据库访问次数上,访问次数越多,对数据库压力也就越大,因此项目中主要也是通过redis进行缓存以及RabbitMQ进行请求入队缓冲等来减少对数据库的访问。. m( P! T P3 m& x
高并发秒杀3 Z. B: J$ ?2 K% O( L
$ V2 {* X! {& {+ F( z) E
一、项目结构搭建1 ^( d- {4 [" U3 D9 p% l
1、集成Thymeleaf * Q$ t6 _$ {, H7 A2、集成Redis' }5 X* p$ i) M* c
3、JMeter的使用 2 Z6 D0 Y. t; A; W6 D4、Result的封装 $ ?! Q/ g$ u5 R( {二、功能实现0 K/ }% m; d. I7 Q* S: {0 F
1、全局异常控制器 ! ^& r, C$ w5 k" h+ K2、拦截器的使用 z1 N9 r& B# w3、分布式Session的使用1 F* ?' I s; `6 x3 u* s: g& V( D
三、页面缓存优化$ z% M+ t3 ^1 u; S: L% P+ I: P
1、页面缓存! K+ k5 W* @6 y+ _: Q
2、对象缓存 ; M9 `; q$ I- Z# g, k3、页面静态化+ ~/ j+ ^8 R. Q% `+ p) m
四、接口优化! G4 ~2 A* X: ]5 C
五、安全优化9 A" I# w( i3 y" \
1、秒杀接口地址隐藏3 v, u. x, X% p
2、数学公式验证码. n @1 G8 V e1 L }3 x
3、接口防刷 + S1 |. g1 h- O% ^" S, t& \4、用户存储/ P5 \1 N+ A6 d* L
六、个人感受 1 _1 D+ Q" K4 u& j一、项目结构搭建+ a: F; ^' `) O1 G+ U# \% k1 d& R; b
& h& N; T& W% Y+ N" u
1、集成Thymeleaf 7 r9 D1 z+ a% W# H 2 G2 g; f: A) N+ M" S1 bThymeleaf是一个Java库,是一个XML/XHTML/HTML5模板引擎,能够应用于转换模板文件,以显示应用程序产生的数据和文本。 4 {6 E: e$ q7 r1 xThymeleaf旨在提供⼀个优雅的、⾼度可维护的创建模板的⽅式。 为了实现这⼀⽬标,Thymeleaf建⽴在⾃然模板的概念上,将其逻辑注⼊到模板⽂件中,不会影响模板设计原型。 这改善了设计的沟通,弥合了设计和开发团队之间的差距。 6 k6 \# Y" z% t0 r. jThymeleaf是一个类似于JSP的模板引擎,但他的区别在于,在运行项目之前,Thymeleaf也是纯HTML,可以在没有服务端的情况下进行运行,但JSP需要在服务端的支持下进行一定的转换。) L# R; A) C) }8 i
Thymeleaf主要有以下三个优点: 0 X1 W$ l6 l/ Q h& F. e& m, k/ d1 Z' E% C: L
Thymeleaf不管在有网络或者无网络的情况下都可以运行,因为它支持html原型,通过在html标签中增加额外的属性来达到模板+数据的展示方式,当有数据返回到页面时,Thymeleaf标签会动态地替换掉静态内容,使页面动态显示。 * ?' t& }2 t' f' e" S XThymeleaf 开箱即用的特性。它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、该jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。 8 ?: _1 i5 c- L0 J" g1 T% n$ WThymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能. * w, {* i! [% _0 U0 G//集成thymeleaf 6 C" t* ~+ ^. u: l1 v% e<dependency>: r* v2 A( [% L' t- j& w2 N
<groupId>org.springframework.boot</groupId> % k5 S/ Y6 N9 C" X3 I5 s <artifactId>spring-boot-starter-thymeleaf</artifactId> 4 c+ |/ z* |- h, t3 j7 ] <version>1.5.2.RELEASE</version> 6 T/ m/ i( A' u6 z" ]</dependency> ! E6 m0 `( o: r1 5 a* S* U, U1 k& F/ }2 , Y; ^: R* Q+ }2 E1 u4 v$ G& t4 m4 g3 4 {6 ^3 v. d% Q5 Z4 g5 b4 P1 M' q% r4 p5 q' ?
5 ) [4 o g$ F* e9 n6 t6 6 X. o0 E% v$ r) ?( m2、集成Redis 3 l. d9 i6 H% H Q 1 v& ]. R3 u- x: uredis的集成,其中使用jedisPool获取jedis,并使用jedis的方法封装在RedisService当中,方便后来使用redis进行缓存5 L+ _- J0 l. ~0 I! K8 V' N
7 t8 H4 A. W( o% k& Y; K
/** 0 }$ J6 _7 q( |4 w. d * 设置对象 " D `; D1 U# o R6 u * @param prefix 不同的前缀对应不同的缓存信息 3 e5 \# b) [ h8 b) V) n/ H2 R * @param key 该信息具体的的key值! c! o5 ~# g- g/ w$ D: v" z
* @param value% S2 M Y, P9 V
* @param <T> % V; l& D v" |6 \ * @return / [) X. `9 M- B1 Y d% n9 w */1 \) w& I- Y* D
public <T> boolean set(KeyPrefix prefix,String key,T value){ . K- p. l- q( O- y Jedis jedis=null; ' Q- [8 f7 h T5 R t& ~% t" E try{( Q, F4 l2 j5 y: v
jedis=jedisPool.getResource(); : a9 Z9 h2 P3 q3 T String str=beanToString(value);# P3 Q0 t; h) C; F* i6 h$ _
if(str==null||str.length()<=0) return false; ) ]0 |+ K$ ~0 z! {6 O //获取有前缀的key, J; N) r: f6 X" O" e
String realKey=prefix.getPrefix()+key; 9 e9 |! V; ?8 ~$ s7 r+ ~ int seconds=prefix.expireSeconds(); ( I: B8 r4 W+ ^ if(seconds<=0){ 7 O# `5 S3 ^3 A jedis.set(realKey,str);' r7 ]5 P5 _: y/ ?
}else{ - C4 W7 Y' _& ?+ A jedis.setex(realKey,seconds,str); x0 Z' }, N* S% H- a) N% P! t
} 0 Z# h' q6 }% v4 D, y2 h! K4 L8 N return true;$ u# O$ z0 F2 F1 o
}finally{ : r6 C8 a* r; l2 C% |1 m returnToPool(jedis);) M( n$ P$ Q6 K% l
} 1 u$ A+ ^& F/ [+ L6 N: h } / n# r# J- h4 U: H) c/ T& ^) a4 N1 0 x: A1 u k j& U2, y5 D- }6 X2 l) o' T
3 % O" @" E, t( N: H5 G+ n/ B47 t- }% d/ Z3 R. Q( y
5 0 ~9 j0 m1 j- r( ~" K% X6 , `- l" I1 e! `& j+ G3 ~: S* X7 ) v* Z3 T2 _' y( g/ V8 6 r6 V. A6 A e4 u1 J2 ^# t N9 % a& Q" l- d& e; Q10* P& c* \0 r, D T5 l$ n) P- i( W# S
11 ; x! I4 ?, ^3 ^8 B1 a0 Y; M9 M12 ' K7 z! i6 P- ?" a% A e' K130 m$ G# S1 r( y/ o2 ^2 Z! a
143 H; t0 u1 j8 c+ q& a, v
15: @2 m' v3 G& ^! i" n
16: X' O8 x' \: x( _
17- h! d7 u2 {% s& b4 r
18+ W8 u: ~8 w+ q, X! {
19 3 N% U9 p9 m) @$ |6 N# l! d0 K20 - p2 W# v2 s6 ~& @21 7 O# H) w1 n/ l4 {& |22 ; P) U7 X3 ~& ^23 * o: E1 M, U2 _- x4 D7 }24: v! e! f3 S: e n) T, U
25& ^( L+ q, q) {4 t
26, F9 F* P, O% q+ y: @
273 ~! V1 d; E( Y* v4 w
而前缀是为了分辨不同的缓存,例如商品缓存、订单缓存、用户缓存等等,其中的expireSeconds则是缓存的有效时间,0代表永久有效。& V8 g4 K/ K* a
$ R6 f) T6 t5 V( J* c( D1 zpublic class GoodsKey extends BasePrefix{1 E0 a8 a% \9 i' s7 m+ ]
private GoodsKey(int expireSeconds,String prefix) {7 J1 l- x4 h5 X/ U$ G
super(expireSeconds,prefix); % ?! ~1 l" k: s! { } / c/ R% Q$ d0 m public static GoodsKey getGoodsList=new GoodsKey(60,"gl");8 T0 C& Z( P; Y4 l+ J0 r
public static GoodsKey getGoodsDetail=new GoodsKey(60,"gd");$ n( N" g9 `, D% @5 N6 }
public static GoodsKey getMiaoshaGoodsStock=new GoodsKey(0,"gs"); & ?0 U$ r$ p6 I5 a( Z5 W" Z( A' F 2 L$ P7 ^, D. H }& w6 Z% N2 X! n/ {! g: A) q0 Y
19 |% J- ^$ G6 m: ^' e
29 K+ h( D! a- i
3 9 W* A! Z( A, L$ ?/ v- r45 T$ Z& t+ _7 }& ]# t
5 ' E; z) r8 W, u9 N6 X6 # M; L/ f7 D. y73 o" D3 z* ~; ^) b: g
8; Z8 r$ {( f" B
9 " H4 H# P- y5 r `2 D7 ^& r3、JMeter的使用 : C( k/ @; w- O% z' o7 b" p 9 k5 q8 e& E9 UApache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,JMeter是一个下载即用的软件,我们使用它来进行高并发的模拟,而它会在进行模拟后给予我们一些可视化的结果,是一个很强大的工具。似乎在5.的版本中,已经不需要去配置JMETER_HOME就可以运行了。在使用过程中,最大的感想就是这个软件巨坑,不知道是不是硬件的问题,在进行500010的并发压测中,出现大量的error,而每次出现error的情况都不尽相同,让人很是头疼。 * j9 G9 i; c! ~$ I+ H7 q4 j8 f: R1 o; v
4、Result的封装/ x' z& Y2 p) T) H) C
* Y* f7 o: q* Y/ ?这是对每次访问结果的一个封装,而CodeMsg类则是对返回码和信息的封装,而Result则是对CodeMsg和对data的封装,在访问过后,我们可以选择生成Result.success对象返回任何类型的信息,异或是返回Result.error来返回错误码和错误信息。, z' a, p0 e+ \
# L' p( p8 m( e; ~/ v
public class Result<T>{3 s% T1 U- x$ o% }. d9 k5 Q9 ?
private int code; / X f, H1 e( ~& x- i5 x! ~, c3 p private String msg; . u& N) f- a5 z# X$ e' V private T data;4 G$ w5 x9 L; H) H
: \- [$ w, G6 E+ N /** ) U- C) x/ W1 ]1 x9 B * 成功时候的调用( Z; l4 [& z) C7 t* W9 U
*/! T- c* J3 j; } x" X' Z
public static <T> Result<T> success(T data){8 \! {! [* @9 d' W! J
return new Result<T>(data);$ @0 a0 j- k- u/ ?8 b# _: w" M2 x
} 8 { I' U+ X, `3 a /** 9 C! Z0 E3 B/ i" `- c9 g * 失败时候的调用 ) T3 @) e: P" t/ M1 m% N */2 l( C! I; C/ H" V0 C: |
public static <T> Result<T> error(CodeMsg codeMsg){ 2 L; ]. A% M# k& D- e: e return new Result<T>(codeMsg);7 Y0 t+ C, ?- M5 |/ D5 c' H
} 4 W/ N; ?* ]) N/ \" o$ t' p4 q/ h6 q9 f& m) `
; j2 n8 n g7 T
private Result(T data){; R/ }2 N( ?, g! v9 ?
this.data=data; : l; ]+ J: V5 ~ } + w$ T. i5 p: j private Result(int code,String msg){ 9 M& g) Y( q2 u this.code=code;$ W6 c$ K8 }) V2 |
this.msg=msg; # b" F1 @& l U9 L2 N } . C, G3 d2 o: V, V private Result(CodeMsg codeMsg){6 f G Y$ R/ u9 @: O% \3 ?
if(codeMsg!=null){ ; ?0 e: ^% R, H6 o3 z this.code=codeMsg.getCode(); 2 @- [1 ^% e0 n/ k this.msg=codeMsg.getMsg(); 6 Q) n4 q% |3 _8 S0 U' U; x' K! n } 1 n$ k/ F, U; C$ O" z e; O }% r) ?3 f& }7 ^1 z. ~+ ]8 G) _
9 p& T- ?; g5 V' }$ e
public int getCode() { K- A3 e5 l: u3 w
return code; - V5 ~' \' ? h& o2 | } " s. {5 R" }- x' a3 u D) X public void setCode(int code) { 4 ~2 o: A3 d. e9 w1 g this.code = code;% R7 }1 a9 p( t, n8 S+ R
} : [% {) Y; \) N public String getMsg() { 2 l* g* W1 c4 J% T" R return msg; # l3 J. [# }+ u0 X1 d9 ^: J }% C5 k) G( U' r4 Q. |! l$ w0 {
public void setMsg(String msg) { - q: M; [6 H) _% m7 }) { this.msg = msg; 6 y1 R% D" M$ q. v! ?- e- ? } X6 y! W5 v7 \ t9 p- ]% J public T getData() { . w% e' w" D/ v2 N$ `' ~; k- p6 A return data; 9 x+ j' w% S2 r' m } ; v4 g2 ^& h9 @- j public void setData(T data) {# N' ]; x$ A- ]0 b5 I6 y
this.data = data; 3 o8 J8 k2 h! x e* U } 0 I# k; d: y+ d' [' Q& [9 \}5 b" p; Y* e. w5 ]
1 m% e5 |( g2 P0 Z9 L' `3 _- L- h* P
2 ! H( O( G6 x* a5 C3" q3 w/ z5 x7 F
4 + j& J1 ^/ H) F+ p4 Z4 d5 & X; @( v% |0 Q" u6. X5 P, P: e; k! ?
79 i8 B( l; _; a! _: [+ V Q$ I
8 - e0 C# _5 n1 K* q+ {95 D$ c# h) s; P2 Q) g
10 , q8 p" N) g( M3 |11 2 {4 _, G' I- C) q3 L8 D* c125 {9 W7 N6 x7 n; Y, `
136 v4 X- }; r1 B( v# l
14, ^2 s9 }2 n- y x
159 c& J+ j. h) E- }
16 & l! G1 k7 g9 T0 u! I/ n8 y179 k( i# c) s* ]: _
18- k2 V, W& ?9 f$ l$ a9 G" z ~
19 7 K7 D7 \0 Z) z; j" l; m( e20& e4 @/ B8 b7 @4 e, o Q. S4 A2 e
21 `* m- u- S9 m4 a22 : ~0 |8 r5 k* I: U6 \" d3 L1 |$ A4 k23 / S8 e8 f& |( W$ V' \3 ?) i& ?% D2 I241 s* v. V+ L. B. e1 ?& g( ~2 U
25 + m1 q# ~; G( a& W26 - m/ B* ?6 c9 g# q4 o/ P% _27; k: L3 J0 C ^' I3 x# Q
28 3 q2 O. E @4 n5 k# ?& e29: j7 ?6 H: w' W9 z% G1 ?" g! O
30 + Q5 @" v, A4 N x31$ s9 ]& D6 p7 K3 A
32 }" i8 b0 I, O) R
33& _7 K* [6 t) ?" @
34 , O9 m3 S, S& _35) j$ y, F% o6 F. g( W
36 Q. w( E2 V% v' p! J* C q37- @' X# w. ]: Z2 \
38* Y$ M1 p: ?: K# B5 Z7 t/ A) h
39. n8 _5 S( R# P8 f# U( Y
40 " k0 e c% Z8 g% ]41 6 E' H8 h: y9 [, o& `42, {3 C/ L/ b1 e. ^' x
439 J+ { S3 l' n# ?2 ^% y
44 4 i k7 |# e9 o- D* y45 : i1 E3 i, n7 z46! F( c0 S8 w" {# X
47 . u! R. _: ^3 T' o3 N48( @1 w" c9 i% z& ^5 t
498 c- y% }& C: V$ b
50" t% ?& C" N( }$ i! a! {3 l
51 - N V3 [3 H3 ]7 D526 R `9 T7 G5 P" Z# i% d/ a8 D
二、功能实现 4 F% a' x' ~& B) U, W- c* a 6 L% t- J6 H1 Q* |功能实现这一块算是比较常规的实现,毕竟这个项目的重心在于秒杀的优化而不在于具体的实现,只是实现了常规的登录,商品列表,商品秒杀等功能。 ; U0 Q) w5 h& Q, R; f2 Y2 Z) d. T
1、全局异常控制器2 Z8 n( m% E0 |2 k. b
% t: M; s5 D9 t) {5 i( d+ d为了使得代码更清爽而不十分复杂,我们实现了一个全局异常控制器。在这个类中,@ControllerAdvice是一个增强的Controller注解,使用这个Controller,可以实现以下功能:全局异常处理、全局数据绑定、全局数据预处理(SpringMVC提供,在SpringBoot中可以直接使用)@ExceptionHandler:如果只使用此注解,只能在当前Controller中处理异常,当配合ControllerAdvice一起使用的时候,就可以摆脱这种限制了。 ) R5 s7 p7 w5 @* o% G$ v. X$ g2 e Q( h I
import com.yuan.SecondsKill.result.CodeMsg;3 l8 s2 Q% [5 ^2 L5 H
import com.yuan.SecondsKill.result.Result; / l7 B' _$ m, y8 i- e+ \5 Simport org.springframework.validation.BindException; ( q E+ V7 p8 |import org.springframework.validation.ObjectError; & B1 o3 L/ k6 @/ R; bimport org.springframework.web.bind.annotation.ControllerAdvice; 4 k+ w. P5 M5 M( ]0 P+ E, a; ~ n" Oimport org.springframework.web.bind.annotation.ExceptionHandler;6 X; M5 d; u5 G
import org.springframework.web.bind.annotation.ResponseBody;- _1 m; v. [6 Z( J9 j9 C
import javax.servlet.http.HttpServletRequest;1 b1 ~- A1 _2 t2 A
import java.util.List;0 [$ T# u# r6 @# A" u- P9 O: l
2 ~1 d) ^" Z- r( v0 T+ V0 o3 r$ K
/** C8 c" ^4 N" {+ n! g" `. z( q$ q" `
* 全局异常控制器 9 W# M: n! d6 w * 拦截异常,在页面上显示错误信息: c/ D7 a. n4 e; Z
*/4 R" @9 c4 |6 B7 U) H
@ControllerAdvice ! ^* g. E% L9 d' k0 H% L2 Z% V@ResponseBody : {0 |+ S' ~) ppublic class GlobalExceptionHandler { 5 D+ j% i. k4 {; \' B* t @ExceptionHandler(value = Exception.class) / i; r. p+ ~1 j) Z public Result<String> exceptionHandler(HttpServletRequest request,Exception e){ , T# z* m) N2 E' }" Y) \5 ] e.printStackTrace(); : w7 Z5 v8 }% v, Z6 `/ L& I) j if(e instanceof GlobalException){ $ w1 o1 E7 K/ c) c, O9 Y GlobalException ge=(GlobalException)e; ; t" P: [2 g! k9 x7 ] return Result.error(ge.getCm()); & Z' p; L7 _( O0 K! ?, r' Z3 b7 [( h } 2 J4 e8 f7 {* D' M/ n9 E: q if(e instanceof BindException){. l6 n) b7 o5 b3 t) W2 P* K
BindException ex=(BindException)e;$ L. Y8 y7 r% z# q9 \& w' P
List<ObjectError> errors = ex.getAllErrors();+ e2 i$ }$ B7 n4 V% W' z
ObjectError error = errors.get(0);/ B7 d! V) }( H8 I
String msg=error.getDefaultMessage();& E0 }7 [2 `9 d6 ~* E8 p2 O) O4 e
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));7 f4 ^2 b* w: y/ {8 g% T
}else{ ' O, q8 `- l2 w% G3 x9 ? return Result.error(CodeMsg.SERVER_ERROR);) M& ], _6 u( n0 S C
} / f4 t# ^* c# x% K+ M5 Y4 _ }' n, c* f1 J. e/ e* w
} $ m% r0 A! z3 P% J: ]1 7 t P- e( b. G7 N8 \8 q5 K9 }1 O2$ H" a% E- E; F A( r
3 6 Y2 }/ a* r" |( J7 Q8 X, P/ R: q+ t2 w4+ j. Z- D8 ^3 t- N+ \/ ]
5 : F: U5 X; E5 u7 H* r6" P5 w- S6 B7 M
7( l( Z! [$ e8 {/ Z& ^5 `
8 4 n9 D# ~: c+ b3 K$ C$ \ S9 R0 x; d9 * _5 X$ n/ t9 e/ h1 i6 c10 9 x# K* H& l& _112 Z( }) w0 X% p! H6 v* O
12 + {- L. v$ S0 ^; L8 o6 Q. w* U13 3 x* z) z5 G$ H) K& A) q" T14& k. w2 j) Z+ @% h* G: d' J
155 C6 i) {* O( s9 h+ ]% t
16 ; H: s; d% H' E& n& Q6 `/ `7 k175 k6 l" j \+ A2 {( e
182 K. n% v% ^$ W( h
196 }* b9 Q# e( Z2 t, V' D
20 6 U( {! l) c/ ?7 ^# f$ _21$ g6 [/ x/ e! \- l
22 . V* o0 D2 }: n+ |238 K7 e, U; i; [" T) |
24& d" T! {" Z" ]) C3 D q
252 a8 ^# J1 B; N: T% m0 c
26 ! z* Z& X! K- W2 q27 ! M+ t. V8 ?5 C5 W$ ~- Q28( ~+ w1 T" A {3 U) c4 q5 H
29 - e2 Z: g+ q5 k, q7 `$ `7 Q% P- I. g309 \# Z& k8 b# }1 \# g8 {6 U3 g
31 ) j3 {5 G# \ H2 F+ Z+ c32$ B6 r0 H% W D* c! }
33 W6 D- I9 s/ p) I1 h: P34 3 T7 x% t7 b) O: x# B. u35 9 i/ V9 y) D1 [$ W& D在登陆方法中,使用@Valid注解配合全局处理器达到对手机号和密码进行格式校验的目的 ' r: s8 I+ ~9 E# j : _2 A7 ]* H6 L' D9 a3 c9 J, K: j @RequestMapping("/do_login")5 g: ^8 b' @9 E
@ResponseBody% \9 c7 b ]6 ^( `/ G/ W
public Result<String> doLogin(HttpServletResponse response, @Valid LoginVo loginVo){: ~6 q% q' _$ v* ]; Y3 d2 ~
log.info(loginVo.toString());! S& a5 h* B) T
return null;9 \" F0 }' d1 {
} - X' B$ @- W5 T6 B( }" p2 i1$ S8 ]1 d, a5 V$ \
2% f) r# w4 @$ H( F/ p
3 7 T% Z2 I0 j- }4 - U; m8 _) O6 f. _* x- [52 ~, j* [* x- V( a. [% W) D& y
65 k( @" \) T8 x7 O/ ?* N2 E5 f2 y
实体类中的注解来校验是否为空等等,其中的isMobile是自己定义的。 + C8 k1 w$ Z' ?5 K7 K! o3 f/ L6 h" ~
public class LoginVo {* F5 W7 H) H& B$ [) X1 y# D" d2 \3 p4 e
@NotNull ! m7 T- Q" x, t# o; F3 G @isMobile) ]8 B. \, L7 T* ~4 d4 H Z* `
private String mobile;5 P _ x! X# G
" x( Z+ T, z2 g, v7 k1 A1 T
@NotNull - q+ y. d9 c5 E4 G. {2 i7 ] @Length(min=6) . o; f4 G- b0 K6 D# O* q3 Y% X/ c' [5 O private String password; 4 ^2 q7 V+ ]4 s: @" |}6 q. [6 ]$ s6 G' n
1 ! @9 q. Y3 f8 f) D7 t2% s4 e8 W5 X8 n& K( @. o) w
3 0 z- m4 ~5 {1 v4 g4 f3 j4( c2 x. \( f8 u* u3 {5 O* N
5/ w# ~2 P; v+ h; ~/ o
6' B/ A, p) N' A& W4 ]
7 & P: j$ { x2 a5 ~- K( J- a8 G8, F" O+ Z' b" O* g
9 3 c4 R4 V }" q# r8 D* P@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) 2 Y% H# L$ f% O! r* E8 w! T@Retention(RetentionPolicy.RUNTIME)+ u, S p6 w8 k$ I
@Documented 5 l& i- O5 ^0 T0 `3 w! g@Constraint(validatedBy = {isMobileValidator.class}) 4 t: O3 p! g) X5 Mpublic @interface isMobile {2 k! z9 @! O& c" }+ [9 ]
boolean required() default true;8 n+ S' m4 f+ i1 f9 `; N$ e0 s0 i3 o
# M! X; J2 p. S8 i0 v) L& N7 H
String message() default "手机号格式错误"; " N6 g6 V4 w) }2 g9 _$ _1 Z0 M$ T9 m; V1 G" [( s; l+ P) Q
Class<?>[] groups() default {}; + ^" z- R7 |% H ! B! J& T; W K7 E Class<? extends Payload>[] payload() default {}; 9 `. h% c, T/ W. Y: @} ; V! }$ I9 ^) \5 m; P, L1 P$ R1: L) }& m. g& u8 o
2 8 ^1 h% l. r5 s# n$ W8 n7 v R38 E9 W0 b( K" |6 q# l
4 - ~9 o- {: \7 B5 4 Z, g( }0 c5 s& _6 " r' v3 O. B3 [! O ~9 ~; I7 7 }8 {) S( U: C/ ]+ g86 `1 B3 \8 u2 |4 l8 y
9/ L9 W7 p' Y; p; n
10( Z$ y) I+ l; v- F3 D& u
11; \4 Q/ M9 U4 u2 C6 a# g' B
12 ! {* O* \* ~: e1 d* J13 $ e# v4 _3 d" W E+ v2、拦截器的使用 - X. W5 c8 x" j& o* }, ]% U( K , d$ z7 w. ^$ I( N4 m/ g+ H2 L/ c使用拦截器与实现了HandlerMethodArgumentResolver接口的类从session中获取user对象,并作为参数注入Controller中。1 h5 b" q! O" y* v* p
3 b+ @! x& B0 G' Y n3、分布式Session的使用 & ]/ m- [( u! G; H1 l" V6 ]2 a8 R8 f3 [* I- W" `! N) R' x
此处分布式session的实现主要是在登录的过程中,为用户随机生成一个token,并保存到数据库中,随后封装成cookie返回前端,并且,获取用户也由这个cookie中的token来实现。关于分布式Session一般有4种解决方案,详细可参考这篇文章:分布式Session的4种解决方案。项目中所使用的就是其中的利用cookie记录Session。 2 i# I3 F, M) T' o) k " W9 O) @3 ?' u% c' M& i三、页面缓存优化! ^$ }& K' @2 D8 Q. h5 ]1 Y
9 _, P1 S2 M2 R5 D2 F- l( r; |2 _& G页面缓存你优化主要使用redis完成了页面缓存、URL缓存和对象缓存,并且使得频繁访问的商品详情页面和订单详情页面静态化,成为纯html页面,使用ajax异步的方式与后端交互,实现前后端分离。另外还有静态资源优化和CDN优化没有实现。; ]4 o& J* y! B2 U+ w5 S
& p& Y1 T3 c9 U* U
1、页面缓存2 w5 ^. t4 K- R! c9 S1 Q' B& U
8 Z( w/ V4 q2 N; v8 y+ D t1 K
这里是一个商品列表的展示,可以看到前后代码的区别,刚开始只是直接去数据库中获取商品列表并返回到前台的页面,前台页面获取其中的信息进行填充。而下面的代码则是对页面做了一个缓存,流程大概是这样的:从redis中取缓存->从数据库中取数据->手动渲染模板->返回html。7 O4 ^/ K+ U9 e! `; b
首先从redis中取相应的缓存,如果有直接返回,没有则到数据库中取商品列表的数据,并且进行手动渲染,渲染后将html页面存进redis中,并且返回。在redis的保存方法中设置了过期时间,默认是60秒。$ o h4 [, p T4 J0 f9 w$ z9 o' `
( N5 d3 S1 a2 e
@RequestMapping(value="/to_list") 5 D, c& y5 l* m" ^6 n) W: K- s* } public String list(Model model,MiaoshaUser user) { + m) L: r; c1 r/ k( T: | model.addAttribute("user", user);7 x% K( G+ w$ F) l6 |& S: m2 U* R
List<GoodsVo> goodsList = goodsService.listGoodsVo();3 Z7 n: `& e7 t# p. \ ?$ J
model.addAttribute("goodsList", goodsList);9 ~7 e, F5 L9 i# m
return "goods_list"; Q; q4 P% h" {! V; | } ; T, z& k D U- X1 z1& {3 w7 d8 E7 p: H% P X5 r
2 7 P5 H' M7 y! G, f3. j* Y( t: K. y- R8 y1 q8 B7 ^
4' T" x* @* x2 x5 ~% M
5 1 D0 n6 c% Y% G, M* \3 `6 : X7 }/ ~8 i7 W! P/ Q) G2 z7 / n6 p) t ` e/** & g }5 D" f8 h A4 W * 页面缓存:取缓存、手动渲染模板和结果输出 ! X5 H2 t2 H5 A5 Y9 \' `* X4 @ * @param model 1 [$ K/ H" j% r7 k/ B * @param user ( m! Y( F. h& l0 E3 s * @return : ]1 r s$ |8 d4 P */8 }0 [7 y0 [; e' i2 \
@RequestMapping(value = "/to_list",produces = "text/html")8 z- B# x' G7 F3 `: F
@ResponseBody " h9 {, F% ?% _$ A5 ~; U public String toList(HttpServletRequest request, HttpServletResponse response, 7 x- N9 H% j' t Model model, KillsUser user){) M# X& G/ N9 n4 b3 r/ ]( n7 o
//无cookie返回登录界面 7 j( U' e/ Y) v* F! E' o6 m- a8 s model.addAttribute("user",user); . ^0 |/ x5 ?, |+ D //取缓存,如果redis中有相应的缓存,直接取出使用 , D0 |( X, t+ P/ U4 j String html = redisService.get(GoodsKey.getGoodsList, "", String.class);6 ]# {9 t& s, u: W
if(!StringUtils.isEmpty(html)){2 T6 ^7 U; D5 f( z" w' F
return html; + {5 o1 B4 z0 j: ]3 ^) r } 4 s. I7 n: v4 H- @1 w3 k% f //查询商品列表 + W, N: Q2 z$ G List<GoodsVo> goodsList = goodsService.listGoodsVo();8 R' n& ^' W" M
model.addAttribute("goodsList",goodsList); / E) m9 X. K i //return "goods_list";2 n% _* V# I$ ~" d0 p8 c3 f3 T/ x( P
3 r' w6 _, d; K! N& W* X9 x+ E
SpringWebContext ctx=new SpringWebContext(request,response,request.getServletContext(), 7 m. L' _6 t% l request.getLocale(),model.asMap(),applicationContext); # J4 I& J+ \0 S \- T- x4 f //手动渲染模板并且存入redis,框架渲染的具体方式也是如此 ) v: t, o3 c$ J& y) v0 L html=thymeleafViewResolver.getTemplateEngine().process("goods_list",ctx); & }% J! E' _9 N# `0 O4 t3 z' h if(!StringUtils.isEmpty(html)){ * T4 F' }1 f2 Z! d4 C0 @; y redisService.set(GoodsKey.getGoodsList,"",html);9 K( A( {4 [, C" A
} 1 k+ ~" c% n' i, I //结果返回 v* B: m, S4 z' H+ r
return html;1 \9 S* y! c, B% j
} ! j. t; w! W$ q$ r2 P2 f2 m1 0 f; p9 J5 F. g, m& J26 s& _; G" a: {$ i
3 ) T' X/ {7 q1 A. Z5 A3 [8 ]. [7 m+ n4! W% C" H+ ?2 N, m/ N$ U4 G
5 9 u) l# l. `' T/ s( m D6 & s/ x# R1 U7 ?" O7 ( M1 K/ r- i. }# C7 g8; [( f, V( D# p" T0 Y7 H2 P) x% I
9# u5 G, q8 F: R& I
10( a3 w8 Y( b/ [( V; Q+ k N& M! {
11 6 O" k9 I+ r2 s) ]" V124 v1 P- u, ], i% Q. {
13 $ r! [3 F! U9 X( K: U; M; G, u14% M+ ~0 k; U ~2 V4 v' a$ @
158 b* ^( ~) b* v5 Z
16 + Z5 R3 L5 h, w" U6 e3 G- d) M1 T5 S175 r& m2 R/ o2 J
183 H& v9 y0 H" @3 A4 L
19 ! z2 \6 Z( ^" S" a* `% O20 / b. m: w! s* _* }; N21; Z0 a4 t% {! g) {
22 * Q0 R4 r+ @. e8 F4 ?23' P/ r. B1 G& S
24 5 a" s. y0 J6 v- T2 _25% V( @6 i }( ?% _' h
26 2 k+ e& g/ s# N: G+ b27 + z0 n$ O8 D* u1 P, U/ o28 [4 z3 V; \% R2 c+ \6 D5 |290 h6 K; D2 C/ c5 j+ m
30 0 w" W# V9 k j# x! o7 D% K0 f9 y31& y& u( q1 ^7 }0 t. f D K
32 8 \! I2 S" K {# L7 B- \2 ~9 O0 A" z7 D5 s0 B4 l/ m2 A) c
9 w+ r# J" X3 H: ~
2、对象缓存 3 b: ?- G0 D% s + O3 i9 H: `/ I3 n根据token对秒杀对象进行缓存,如果缓存中还有秒杀对象的信息,则不去数据库进行取出。/ W# K' w1 Q+ e" d: @
, k( |* D9 B6 |2 p' [* j: x1 ~ # M c. l2 @& L! c' V0 ]3、页面静态化 ' B) _( ~3 t4 I2 O9 i6 @ : a, `1 @. Z0 M4 Q页面静态化常用的技术为AngularJS、Vue.js等等,此处使用js简单模拟该过程,实现前后端分离。页面静态化,其实就是把用户经常访问的页面做成一个静态的页面,避免每次都要从数据库取出数据并且渲染到对应的模板文件中,从而减少服务器的压力。- P% j, W- u5 b7 O: Q6 G
页面静态化是将动态渲染生成的画面保存为html文件,放到静态文件服务器中。用户访问的是处理好的静态文件,而需要展示不同数据内容时,则可以在请求完静态化的页面后,在页面中向后端发送请求,获取属于用户的特殊数据。5 Z- m0 H5 G2 w9 k" u; _
页面静态化是一个以空间换时间的操作,虽然页面增多时会占用更多的空间,但提高了网站的访问速度,提升了用户的体验,还是非常值得的。 2 v6 m6 s3 B, ^( N/ }) `关于原来的页面,正常的前端访问后端接口,获取html页面,展示,而页面静态化,是利用ajax异步传递数据,在前台进行数据的填充,实现了前后端分离。 - k& K0 ^" f3 ]/ L' Z7 U. N$ S: l; h* [; b3 a6 k2 U
@RequestMapping(value = "/to_detail/{goodsId}",produces = "text/html") / R+ i5 G( T, F, U5 B @ResponseBody) ?1 h: b1 p0 I/ q7 Z$ V
public String detail(HttpServletRequest request, HttpServletResponse response, l9 K9 Q5 o, O! B6 D7 O0 ^, F
Model model,KillsUser user,$ Q3 E4 J- \5 L/ m% E# f! ^+ t
@PathVariable("goodsId")long goodsId){ 2 e+ o ^* _2 n, u; @ R' U& S model.addAttribute("user",user);% G' T2 j7 i; u7 {! ?
% s. E2 @/ E" A) I
//取缓存,如果redis中有相应的缓存,直接取出使用 & T& C$ r9 F' k# x# J3 K String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class); + H5 I7 {* j/ M' Y5 u if(!StringUtils.isEmpty(html)){ : Z; g/ K- F4 H( ~5 x return html; - T0 Y4 e7 J2 k$ q } / @8 I/ ?1 S4 `% P7 ]. b ( {# ^. ~- D/ c! s9 J$ U. R6 w GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId); 8 ~8 k! Y& b Q) K4 U8 } model.addAttribute("goods",goods);9 b2 o7 l$ }8 A# E/ u
2 h3 _/ g9 f& g long startAt=goods.getStartDate().getTime(); ; T1 w f% x& g+ {& a long endAt=goods.getEndDate().getTime();0 Z( n; p$ `* n; @: V
long now=System.currentTimeMillis();8 I& h0 I) D/ E- @8 E$ z
9 R: d3 @; ?" e6 _ int KillStatus=0;//表示还未秒杀6 P0 {' L' p% G+ p' ?& l
int remainSeconds=0;- [/ i _- F% m& k2 V
/ ]% S3 i1 S. f
if(now < startAt){//秒杀未开始$ {5 Y0 b5 x9 Y: H$ ~
KillStatus=0; + J9 }0 ~6 D& T+ K remainSeconds=(int)((startAt-now)/1000); # u# ^& M, A' f) ~ v8 j' K }else if(now > endAt){//秒杀已结束" `( l9 g9 b1 h2 [# H
KillStatus=2; % F- V9 c& t' [8 e$ |( R3 Y remainSeconds=-1;( Y4 F% w& I8 ^0 t, G8 t
}else{//秒杀进行中* Y, s0 X8 }4 z+ X) M
KillStatus=1;( U$ j$ r, F2 F% z& @
remainSeconds=0;+ c& E7 k# V7 v7 \9 {4 s3 }- z
}1 C+ _/ w% y! \
model.addAttribute("KillStatus",KillStatus);9 h/ v5 Y6 Z: f, a5 U2 ?& B0 K
model.addAttribute("remainSeconds",remainSeconds); ) A; p' F5 c3 D% V9 g5 K" ` " m& y5 h8 g; \& \& Y! E1 c2 o- W2 ]: ?! R9 Y
SpringWebContext ctx=new SpringWebContext(request,response,request.getServletContext(),6 y+ ?) f3 Q; O$ h
request.getLocale(),model.asMap(),applicationContext); 9 @0 G( V9 |6 p% y9 G! M //手动渲染模板并且存入redis,框架渲染的具体方式也是如此8 @5 ^# A5 M) } t
html=thymeleafViewResolver.getTemplateEngine().process("goods_detail",ctx);0 Q* |+ B, `. Y/ P) Z
if(!StringUtils.isEmpty(html)){ 1 F4 x; k+ M( _5 ~0 F redisService.set(GoodsKey.getGoodsDetail,""+goodsId,html);2 ^% O) V# _3 h% n) J
}* C: S5 F0 S* O9 a9 P9 F
//结果返回 7 A7 I+ Q2 t0 ^3 V return html; $ @$ r$ ^$ F: q, U& O% {/ U //return "goods_detail";2 \* d) b: o4 }. i2 t$ H& K
} - n9 h: K4 k5 {* _1$ x- ~0 j/ v' P- Q
2 + L! X# M6 u$ S6 J. t7 |; f3 % T' i e: a. j4 0 k- W$ E8 _ I# K5 7 g8 M0 U6 d6 u7 ~& @6 . A# ?6 s3 F, q7; H2 [3 ^2 r; G+ L6 Q* m
8 + Q* j3 b5 f3 D4 F! G) J' Z9 , m( S0 i; h5 \3 a e: [8 v10/ m, _: h" Y$ D* }* H! A3 t
11, ~6 W/ B4 h e, k3 K
12% ]# F* P p! k' G6 A
13$ h# @) A, A1 b
14( n4 X- t- s, A+ h# g" C
15 ' {# P2 d) e& X' Y3 {" w16 . f: t& p$ y( p( h; [4 v4 T6 z17 + D% m" h* [3 C6 e18 ; `) q9 r+ ?/ l196 _( c1 O4 Q& w& {
20* H2 f4 o8 S( _1 @
21 " D/ L3 a8 K, d8 M% K: B R9 y$ W227 S a0 J9 W( N# B$ z1 E2 Z
23' r0 \* x/ o. N, J- E1 P8 n
242 W% L1 r* J% C B& E8 b" z
253 s1 O8 E9 i1 V, K5 x: u0 [) u* E
26 4 Q5 m2 J7 D9 K27 , K K1 U) C& U6 Y6 i+ ]28 , ~' H; [! H _9 k, `29 0 {) m, A$ O' N" v$ Y' Q {30 % m0 B3 q4 a6 H2 m% B31 ; e' @) h- I7 t V4 _32 . w, B! w- R5 [1 B7 M" P$ ~33! r6 ]1 h4 d6 R2 z" |
34: z, S+ ]4 X9 I6 w
35 7 D# }3 J- v' M+ G2 i36) o% P) E$ @% L, p/ F2 q, R @& n0 |' x
37$ s; T% r: w" Z$ I% t
38 + c& g% _ O/ D* A7 _4 P39 ' s/ l7 o9 F- S2 z4 y40- F) R( Q+ M8 [/ e
418 z: o: H2 v7 |
42$ J, x3 P: D2 t: P0 A N
439 ~( b5 {, U2 Q* [ h
44: D {4 H. y' q @0 J
45 * T* {; G6 q- c/ l" x46 8 z/ S5 i) C& x* X( t$ y47 - q- R& D: s' U, M. m48& q! ~3 ?6 k0 P6 r
前端代码通过ajax异步请求后端的接口返回数据,并在前台取出数据对页面进行填充。- ?5 k4 k% n" _1 p2 h) X* r: I; G
7 c$ R2 X" D+ K4 u" r/ p//前端 6 c/ S; X+ q7 ^: q7 F M5 d# L5 Y$(function () {" L9 d* h7 p& K2 [( k' |/ `' L
//countDown(); s9 X: ]% k" V+ J
getDetail();! a; P3 f3 \9 I& s9 ^6 O( O
});% d6 n% p9 z$ }: j2 a# x
function getDetail(){ " j. N1 s8 m6 i. P var goodsId=g_getQueryString("goodsId"); 9 M6 H- v) \( t. W $.ajax({ ! R5 f( p+ Y$ R/ [ url:"/goods/detail2/"+goodsId, 9 {. z8 c6 v* S( A" T. F5 Q type:"GET",8 }/ w0 u6 X; u" `- P9 L2 Y
success:function(data){ 4 y! |& X2 K, s/ ?; v/ C if(data.code==0){ ! e# c0 q5 b5 e! @+ E9 u! P: o render(data.data);+ q/ \$ X! u# M1 i) ^; n, ^
}else{ . f* X+ C8 t9 ^. C5 X/ C layer.msg(data.msg); k# P0 B: z+ _9 t- q7 p5 Q }9 C: r6 {, e' b6 _
}, ; ~) K9 I% I& V error:function(){ ! T* p4 M: o# s* o layer.msg("客户端请求有误")6 D2 a/ ]; R! W2 K7 z6 ` _
}* t" ?' l; I. ^. y* b$ {$ Y1 |
, U) A) [) Y6 { s/ V });2 [- o6 E. i" l
} ( r* ]! p- o$ g* y/*text() :设置或返回匹配元素的内容" {2 c W. e1 o) v) @
* val() :设置或返回匹配元素的值6 P: t! ^8 g' S7 N- F
* */ . j3 g6 H- L- w9 Gfunction render(detail){ 6 c% K5 |. I1 M% d# z- ]$ p ?1 k var goods=detail.goods; & ?8 _( F M6 t- ~5 W) u% t2 ` var KillStatus=detail.KillStatus;" b+ Z" s \; o! W2 j
var remainSeconds=detail.remainSeconds; 6 k. s2 l I" j6 L var user=detail.user;. c; J# @, ]' D' u
if(user){9 F) h Q! A3 [% u
$("#userTip").hide();/ R: S% h2 C7 @3 s' V; L; T0 `
} # m1 O' o0 O- m: N: T $("#goodsName").text(goods.goodsName); ) L N( h9 A6 m$ D, Z C $("#goodsImg").attr("src",goods.goodsImg); 2 X1 D5 ]$ I6 x' \ $("#startDate").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss"))& P+ j$ i2 Z4 r, s: o# P
$("#remainSeconds").val(remainSeconds);# `: y, C* s5 m/ E
$("#goodsId").val(goods.id); : _: C# k! f7 h% V; e $("#goodsPrice").text(goods.goodsPrice);. R6 D$ n* v5 l
$("#miaoshaPrice").text(goods.miaoshaPrice);5 F; R' n- N ?9 E! R* X2 o* w
$("#stockCount").text(goods.stockCount); * _6 S9 y3 y8 d+ j, z countDown();* _2 F: z/ O% {
}* b6 `4 s g2 A8 T
//后台6 S- l0 u; d7 M0 P
@RequestMapping(value = "/detail2/{goodsId}")/ V' @$ F4 f4 w8 L Y; u/ `9 q0 o
@ResponseBody/ K( j3 k0 ^" S$ X, T
public Result<GoodsdetailVo> detail2(HttpServletRequest request, HttpServletResponse response,4 p, y$ ^ c% q" f9 c
Model model, KillsUser user, ' r3 N0 e6 [ Q* ]" h @PathVariable("goodsId")long goodsId){# v" Z; L' H9 [1 y
GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId);" \/ V) y( c3 a* @
Q; @1 K- u. V9 G0 H2 ^# o
long startAt=goods.getStartDate().getTime(); ( s* S0 h7 i) k6 W. K/ {# @8 J9 ` long endAt=goods.getEndDate().getTime();; j8 @6 [, f$ ~: I. c& r
long now=System.currentTimeMillis(); 1 D) `5 l/ s% v4 m n* y4 u: f, ^) E: J( v
int KillStatus=0;//表示还未秒杀 ; |0 f9 I! k C$ x. [' w* [$ r int remainSeconds=0; ! ` f8 `9 R x7 K, Z/ L 6 W+ X/ k/ d; W0 ?/ ~5 b& u1 s if(now < startAt){//秒杀未开始! K% c; u$ O) Z* Z
KillStatus=0;4 V" i$ U! y: p
remainSeconds=(int)((startAt-now)/1000);6 K' Y7 U2 m c; w' B$ f; X
}else if(now > endAt){//秒杀已结束 7 l" e w& r/ W' C9 K- k# s% r KillStatus=2; & T8 {" ]" S8 g: \- l$ e remainSeconds=-1;) T5 O0 B& M% G7 E! X: q/ \: u
}else{//秒杀进行中" T+ |" Q8 W/ @! `1 w/ z: H+ t
KillStatus=1;3 f4 }' O9 `$ z- t( h1 t! Z
remainSeconds=0; / ?* g0 c5 K0 j+ a) @ } 9 r9 j/ ?/ [& Q4 e# X2 J9 J9 z% M , i1 N. P( \% w5 ?5 P) c GoodsdetailVo vo=new GoodsdetailVo();5 O* \- `% n" i% X
vo.setGoods(goods); . u+ L! j$ i% S- } F- q8 n' A3 H vo.setKillStatus(KillStatus); 4 v5 [3 `' x6 M8 s* G/ B" W vo.setRemainSeconds(remainSeconds);( y4 B& ~, n6 W0 L9 d+ l
vo.setUser(user); " s3 j9 b) A$ w" U return Result.success(vo);9 N/ P% ^1 ~7 i O
}) \" b8 k) ~) c+ |" @& Y! \
1 & _5 v) Y2 H2 |: p8 b/ P y22 d3 T+ g& A8 P1 U, P9 _% j
3* X! }( l T1 ]# f
4 6 _" a6 e( X$ T5# ?' J* c; M2 g* x$ f/ P
6 : r1 z- k, }) O4 o6 K% B, T0 j6 l" u7& u' U) W+ `5 D" e
8 3 u6 o5 I4 O) E/ {6 c& ^9 * e: ~2 V- |2 m7 @, L+ ] y10, ?+ x Q8 f3 J; T! M; U
11 * p2 i' Y& P! m6 t12 % B1 E3 S1 U* L1 T5 `1 `" x138 A8 F$ x! W6 q
14 ! A; A" t8 i# \: M& ?1 D15 u: Q X" k8 a16 0 R6 T! c/ z: Y17) Y6 [1 C+ U" B: Y: G/ E0 J T' a
18 ! V. e q: g! {6 r) X19! N; ?0 n. F# e1 `" ~' [& W _1 h
20 " X# I# c+ U7 y$ v21# e5 t5 {2 }* B0 g6 U
22 2 X7 e) F! _) ~# d4 e, Z4 u. s: L3 A& y, S23 - A/ t8 V$ R- ~& X24 * V* b3 e0 m Z6 H( B25 `* d, Z# P [2 s+ I9 S26; ?7 K: V/ W2 I, C8 n1 g( b
272 u2 N9 F. g, x7 R, m9 z
28 - m8 r4 m' N4 V4 m! c0 Z29 , S* @3 R/ `* y5 O( V0 C) n( H8 @30 - N- u+ [, k" c6 E9 W* X7 {31 ) J/ g/ N0 K- L+ P32 & h+ w5 ~. {7 \/ g7 T33 7 u: R* J" O9 t34; w- ^$ o2 K" \/ v# r
35) h* v% \: a' B8 N, A c
36# _ ^. ]7 a4 R3 \! i
37. ~- Z6 j, x9 q$ [
38 T8 ?( z& m- I, C
39* d$ T9 T- R" E8 u l
40. |& Y; U( T/ [: X
41' ~+ {/ A9 u% |: i/ X* P
42 # m L0 \. Z4 ~8 B. U' W# r43 ( u2 k% _2 j s: l3 d! S44 3 U {8 Z' O @: B6 u451 I6 _& @3 S8 A6 p2 A! O
46 ) f$ s1 P0 P3 \% l: {! J* ~6 M4 F473 V6 F0 w7 H1 R$ U1 G
48 / q% l$ k) K3 v- U' l49 0 h; E. B6 p, C2 Q! N. O50, L+ F$ ?9 @$ ]: p
51! u* f) r" ^0 @" p5 p+ Y
520 I0 x G- b; u5 f l
53 . k, f9 w8 ]& ?3 }2 P9 G54 0 j" e# } P4 w1 j2 L# {55 ' }% Z( Y) w9 Y; V56$ X# b* _) o% d d# P3 {
57# u, K6 G* p/ X5 {: B7 X3 ]
58 $ T3 Q! i' t% b1 C59 ; i% h! u2 E4 g60 : a) r8 i/ n% T$ |61 8 }7 Q9 L' K N0 t2 ^4 G3 \3 K62 ! R' s, p* g* K6 f- x) H63( E4 K9 v+ u" x
64 / q2 u. |- s0 w# I! x65 2 V" r. D" u; y" K1 u" Z66 6 T Z+ s5 _3 i# s. M1 \67 & ~, Y' @0 P s5 C# Q683 r# q/ }6 F# w/ |1 Q+ J/ a
69+ r0 J: h# g# h9 ^! E
70" R6 L, Q& u$ s, h! {
71$ w* x; K) u/ z8 h/ [
72 " p& W( V2 r+ ?0 `! [3 U' V' \73, Q8 g) Z8 ^1 t' E% K! ]5 A' e2 w
74 $ C0 _' {2 _6 v5 n! l75 7 W7 h# C& s+ B6 `) |, |76+ Q: P s1 A {
77" E' U) `/ Z0 p' C4 c: _
四、接口优化6 v0 O3 [* ]0 \) M8 x: J