秒杀项目分析

一、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。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8MDQAnDw-1608779284020)(C:\Users\Tanzhiyong\AppData\Roaming\Typora\typora-user-images\image-20201222151423847.png)]

注意: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的方法实现。

  1. GoodsKey.class
  2. MiaoshaKey.class
  3. MiaoshaUserKey.class
  4. OrderKey.class
  5. 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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-agdIFu9T-1608779284021)(C:\Users\Tanzhiyong\AppData\Roaming\Typora\typora-user-images\image-20201222165528153.png)]

② JSR303数据校验

1)导入依赖

	<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

2)添加注解

添加注解 @NotNullLength(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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dGHN3rHd-1608779284022)(C:\Users\Tanzhiyong\AppData\Roaming\Typora\typora-user-images\image-20201223190224308.png)]

读取数据库mysql比读取redis缓存耗时用的多。

2)Redis压测工具 redis-benchmark

3)springboot打war包

五、页面优化技术

参考:https://blog.csdn.net/qq_36505948/article/details/82620908

1)页面缓存+URL缓存+对象缓存

  1. 取缓存
  2. 手动渲染模板
  3. 结果输出

更新缓存的四种设计默认:https://blog.csdn.net/tTU1EvLDeLFq5btqiK/article/details/78693323

我们在这里采用的是 Cache Aside Pattern模式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nT4KhdDk-1608779284024)(C:\Users\Tanzhiyong\AppData\Roaming\Typora\typora-user-images\image-20201223214841344.png)]

**页面缓存:**将页面缓存在redis中。缓存的内容是thymeleaf中的视图解析器解析的内容。(细粒度最大)

html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);

**URL缓存:**针对不同的URL信息(比如ID等),缓存不同的页面信息。比如针对不同的商品ID,缓存不同的商品详情页面信息。(细粒度比页面缓存小)

**对象缓存:**将对象缓存在redis中。(细粒度最小)

缓存的时机:controller中的handler执行的时候,先取缓存;如果缓存存在,直接返回缓存;如果缓存不存在,从数据库中取相应的内容,并将其缓存在redis中(有时候,更新对象的时候,需要删除旧的缓存,再添加新的缓存)。(缓存有默认的过期时间)

**页面优化效果分析:**通过第四部分的压力测试,主要查看QPS(query per second,每秒查询率)来判断页面是否优化。

2)页面静态化,前后端分离

  1. 常用技术AngularJS、Vue.js
  2. 优点:利用游览器的缓存

3)静态资源优化(图片,css,js等)

http://tengine.taobao.org/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mMdeXkeb-1608779284025)(C:\Users\Tanzhiyong\AppData\Roaming\Typora\typora-user-images\image-20201224105827935.png)]

4)CDN优化

内容分发网络(Content Delivery Network,CDN)是建立并覆盖在承载网上,由分布在不同区域的边缘节点服务群组成的分布式网络。

理解cdn就是有许多个缓存服务器,访问经过cdn加速的域名的时候,会自动访问离我们最近效果最好的那台缓存服务器。

在这里插入图片描述
在这里插入图片描述


版权声明:本文为weixin_40571937原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_40571937/article/details/111614350