秒杀项目分析
一、redis
① redis相关设置、类和接口的作用
1)application.properties
redis的配置信息:application.properties
#redis
redis.host=localhost
redis.port=6379
redis.timeout=100
redis.password=123456
redis.poolMaxTotal=1000
redis.poolMaxIdle=500
redis.poolMaxWait=500
2)redisConfig.class
定义
redisConfig.class
来读取application.properties中设定的redis的相关信息,注入到Spring容器中。
@Component
@ConfigurationProperties(prefix = "redis")
public class RedisConfig{
//...
}
通过两个注解的组合使用,把application.properties里关于redis的配置信息注入到spring容器中。
3)Jedis
在Jedis 和 RedisTemplate 技术选择上,选择了Jedis来实现redis功能。
1、jedis的创建:使用JedisPool
;
Jedis jedis = jedisPool.getResource()
而创建JedisPool(在redis/client/jedis包下)的需要对JedisPool进行配置(导入RedisConfig中读入的redis配置信息),所以创建RedisPoolFactory.class
,调用Spring容器中的redisConfig这个Bean,注入application.properties中设置的关于redis的信息。
@Service
public class RedisPoolFactory {
@Autowired
RedisConfig redisConfig;
@Bean
public JedisPool JedisPoolFactory() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal());
poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait() * 1000);
JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
redisConfig.getTimeout()*1000, redisConfig.getPassword(), 0);
return jp;
}
}
2、使用jedis操作Redis:
创建RedisService.class
,@Autowired自动注入的方式注入jedisPool,通过jedisPool获取jedis。
注意:RedisService.class
里的方法,跟默认的RedisTemplate的方法有点区别,大部分方法都添加了一个参数 KeyPrefix(KeyPrefix是接口,可以使用他的子类,详见下一讲)。例如 set(KeyPrefix, String, T)方法的理解,就是先通过KeyPrefix和String组合获取redis中的key。
4)KeyPrefix、BasePrefix
关于redis中的键(key)的前缀
作用:通过键的前缀来区分是秒杀商品还是用户的缓存信息。
1、定义了一个接口 KeyPrefix.class
:
public interface KeyPrefix {
public int expireSeconds() ;
public String getPrefix() ;
}
包含过期时间和获取前缀两个方法。
2、定义抽象类 BasePrefix.class
,实现 KeyPrefix.class
接口:
public abstract class BasePrefix implements KeyPrefix {
private int expireSeconds;
private String prefix ;
public BasePrefix(int expireSeconds , String prefix ){
this.expireSeconds = expireSeconds ;
this.prefix = prefix;
}
public BasePrefix(String prefix) {
this(0,prefix);
}
@Override
public int expireSeconds() {//默认0代表永远过期
return expireSeconds;
}
/**
* 可确定获取唯一key
* @return
*/
@Override
public String getPrefix() {
String className = getClass().getSimpleName();
return className+":" +prefix;
}
}
存入redis的内容,根据类别进行分别,分别不同的prefix前缀。通过继承
BasePrefix.class
的方法实现。
- GoodsKey.class
- MiaoshaKey.class
- MiaoshaUserKey.class
- OrderKey.class
- UserKey.class
② redis整体脉络
一、配置redis,包括ip,端口号,密码等,并注入到Spring容器中。
二、构建操作的redis的方法,通过jedis实现RedisServer类。(jedis的实现又是通过JedisPool)
三、在使用RedisServer的时候,封装了KeyPrefix类,区分不同类别的键。
二、登录功能
①登录功能实现
1)两次MD5
md5的编码实现是通过调用
DigestUtils.md5Hex(密码)
来实现的。该功能包在org.apache.commons.codec.digest;下。
两次md5流程(都是+salt的):
第一次:用户输入密码 ——> formPass
第二次:formPass ——> formPassToDBPass
② JSR303数据校验
1)导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2)添加注解
添加注解
@NotNull
、Length(min= xx)
、@IsMobile
等,其中@IsMobile
是自定义注解
@IsMobile自定义注解实现:
@IsMobile.class
文件(参考 @NotNull注解配置文件,复制粘贴过来稍微修改):
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class })
public @interface IsMobile {
boolean required() default true;
String message() default "手机号码格式错误";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
IsMobileValidator.class
文件:
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required = false;
public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
}
public boolean isValid(String value, ConstraintValidatorContext context) {
if(required) {
return ValidatorUtil.isMobile(value);
}else {
if(StringUtils.isEmpty(value)) {
return true;
}else {
return ValidatorUtil.isMobile(value);
}
}
}
}
③全局异常处理
1)自定义异常处理类型和异常处理器
- GlobalException:定义异常类型
- GlobalExceptionHandler:全局异常处理器
定义的异常类型,可以携带更多的信息。使用全局异常处理器的时候,抛出这个异常,同时可以抛出我们定义异常类型时设置的信息。
GlobalException.class
文件:
public class GlobalException extends RuntimeException{
private static final long serialVersionUID = 1L;
private CodeMsg cm;
public GlobalException(CodeMsg cm) {
super(cm.toString());
this.cm = cm;
}
public CodeMsg getCm() {
return cm;
}
}
GlobalExceptionHandler.class
文件:
采用@ControllerAdvice和@ExceptionHandler的方式,抛出异常。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value=Exception.class)
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
if(e instanceof GlobalException) {
GlobalException ex = (GlobalException)e;
return Result.error(ex.getCm());
}else if(e instanceof BindException) {
BindException ex = (BindException)e;
List<ObjectError> errors = ex.getAllErrors();
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
}else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}
参考博客:http://39.97.228.130:8090/archives/controlleradvice%E5%AE%9E%E7%8E%B0%E4%BC%98%E9%9B%85%E5%9C%B0%E5%A4%84%E7%90%86%E5%BC%82%E5%B8%B8
④分布式session
场景:服务器分布式集群,同一个用户登陆成功后的信息,维护的session在不同服务商不一样,怎么实现同一个用户访问不同的服务器得到的session信息是一样的。
做法:
1、每次用户登陆成功后都会生成token信息,标识这个唯一用户。
2、在用户登陆时,将token信息保存到cookie中。
3、同时,以KeyPrefix+token为键,以user信息为内容,保存到redis中。
4、这样用户在登录成功后的每次请求,都可以通过cookie,携带token信息进行访问。后端在通过KeyPrefix+token为键,查找是否有用户信息存在。
5、注意过期时间的设置:cookie的过期时间 和 redis中的键值对的过期时间应该一样。
6、当用户第一次登录成功后,例如设置过期时间为30分钟,那么在10分钟后,如果用户再次发出请求,此时我们应该刷新cookie和redis中键值对的过期时间为新的30分钟。(做法:因为每次用户访问的时候,都会调用 getByToken()方法在redis中获取是否有对应的用户的键值对信息,所以如果存在相应的用户信息,只需要在此时重新添加一次 cookie和redis的键值对信息即可。)
1)生成token(将token保存在cookie中)
通过UUID生成唯一的token
public class UUIDUtil {
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}
2)为了标识这个cookie对应哪个用户,将其保存在redis中
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);
}
3)访问Redis查看是否有cookie中对应的user信息
public MiaoshaUser getByToken(HttpServletResponse response, String token) {
if(StringUtils.isEmpty(token)) {
return null;
}
MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
//延长有效期
if(user != null) {
addCookie(response, token, user);
}
return user;
}
4) UserArgumentResolver实现的HandlerMethodArgumentResolver接口
https://blog.csdn.net/songzehao/article/details/99641594
5)简化每次handler方法执行时的验证
该程序的做法是,方法每次执行都带有参数(Model model, MiaoshaUser user),然后执行 model.addAttribute(“user”, user);
三、秒杀( 数据库设计和功能实现)
数据库设计:
1、商品表:商品的名称,价格信息等关于商品的信息
2、订单表:
3、秒杀商品表:对应商品表中的商品的id、秒杀价格、开始时间、结束时间,数量等。
4、秒杀订单表
商品表专注于商品的信息的展示,不包含任何秒杀信息,秒杀信息放在秒杀表中来展示。同理订单表也是一样。(这样做的原因,是因为秒杀商品的频繁变化会导致商品表里信息的频繁变化,使得我们难以维护商品表)
1) domain对象
利用数据库中对应表的内容,创建domain对象(POJO或entity)。对应的是数据库表中的实体类。
2)vo对象
视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。即vo对象可能是pojo对象的一部分,或者是几个pojo的部分拼装成的对象(可以通过继承的操作)。(类似POJO,对应的成员变量都要有get/set方法)。
3)秒杀实现涉及到的页面和类
页面:goods_list.html、goods_detail.html、order_detail.html
类:
Service层:GoodsService.class、MiaoshaService.class、OrderService.class
Dao层:GoodsDao.class、OrderDao.class
注明:
- 该项目的Service层并没有分Service和ServiceImpl,所以xxxService.class直接写的业务代码,而不是接口。MiaoshaService.class中的业务方法,调用的是 goodsService和orderService两个服务端的方法。
- 在Dao层,没有使用 **.xml文件,使用的是mybatis的注解。(额外的在OrderDao.class文件中的insert()方法,通过@SelectKey设置自增主键id)
特别的:在miaoshaService.class中的miaosha()方法添加了
@Transactional
注解,保证事务操作的原子性
四、压力测试
1)JMeter
Apache下的顶级项目。Apache JMeter是一款纯java编写负载功能测试和性能测试开源工具软件。
使用: 在D:\Java\apache-jmeter-5.4\bin目录下执行
jmeter.bat
启动图形界面,后续测试可以参考:https://blog.csdn.net/u012111923/article/details/80705141
读取数据库mysql比读取redis缓存耗时用的多。
2)Redis压测工具 redis-benchmark
3)springboot打war包
五、页面优化技术
参考:https://blog.csdn.net/qq_36505948/article/details/82620908
1)页面缓存+URL缓存+对象缓存
- 取缓存
- 手动渲染模板
- 结果输出
更新缓存的四种设计默认:https://blog.csdn.net/tTU1EvLDeLFq5btqiK/article/details/78693323
我们在这里采用的是 Cache Aside Pattern模式。
**页面缓存:**将页面缓存在redis中。缓存的内容是thymeleaf中的视图解析器解析的内容。(细粒度最大)
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
**URL缓存:**针对不同的URL信息(比如ID等),缓存不同的页面信息。比如针对不同的商品ID,缓存不同的商品详情页面信息。(细粒度比页面缓存小)
**对象缓存:**将对象缓存在redis中。(细粒度最小)
缓存的时机:controller中的handler执行的时候,先取缓存;如果缓存存在,直接返回缓存;如果缓存不存在,从数据库中取相应的内容,并将其缓存在redis中(有时候,更新对象的时候,需要删除旧的缓存,再添加新的缓存)。(缓存有默认的过期时间)
**页面优化效果分析:**通过第四部分的压力测试,主要查看QPS(query per second,每秒查询率)来判断页面是否优化。
2)页面静态化,前后端分离
- 常用技术AngularJS、Vue.js
- 优点:利用游览器的缓存
3)静态资源优化(图片,css,js等)
http://tengine.taobao.org/
4)CDN优化
内容分发网络(Content Delivery Network,CDN)是建立并覆盖在承载网上,由分布在不同区域的边缘节点服务群组成的分布式网络。
理解cdn就是有许多个缓存服务器,访问经过cdn加速的域名的时候,会自动访问离我们最近效果最好的那台缓存服务器。