第一章 概述
解决秒杀高并发问题的方案,大并发的瓶颈在数据库
- 缓存系统
- 异步化
- 横向扩展
技术点
-
Model的作用
在控制器中,数据会存放到Model对象中,当需要生成HTML的时候,模板引擎会根据名字来定位数据。
-
热加载主要是在idea的setting的compiler中勾选 build project automatically,且只能在debug模式下可用。也可以使用jrebel
-
泛型的定义和使用?
-
Result的封装
秒杀的业务逻辑
首先完成分布式session,之后用户浏览商品列表,查看商品详情,点击秒杀按钮,如果成功,则生成订单。
业务逻辑完成之后,进行系统压测,进行高并发请求。
高并发下出现问题,需要进行缓存优化,缓存是个大系统,
网关nginx缓存、应用服务器缓存、页面级缓存、URL缓存、对象缓存,缓存系统应对打并发的方法。缓存要解决数据不一致问题。
秒杀的并发请求太大,如果请求都透传给DB,DB抗不住请求,使用消息队列设计异步请求。
使用nginx进行横向扩展解决大并发。先优化才能扩展。否则请求还给数据库,瓶颈仍然在数据库。
业务系统完成安全问题。
结果封装
- code和msg是一一对应的,成对出现
- 如果不允许修改对象的属性值,则去掉属性值的setter方法
- 如果不允许外部创建对象,则将构造方法私有化
事务
在controller的方法上面添加transaction,可保证对数据库的操作是事务的,它能保证方法内多个数据库操作要么同时成功、要么同时失败。
编程知识
- 一般springboot的启动类MiaoshaApplication放到最外层,是为了方便进行controllerScan。
- 后台controller向前台一般会输出两类,包括1 rest api json的基本输出 2 页面
序列化
序列化(Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。注意,序列化不仅仅只是指可传输,可存储也可称作序列化。
因此,把不可存储的对象转化为可存储在数据库中的json串也是一种序列化。
其他
- 对系统模块的拆分:登录模块、鉴权模块、安全措施、缓存系统、Result结果封装、日志系统
- 应对大并发思路: 1、缓存 2、异步 3 横向扩展
- 查看springboot的官方文档时,搜索srpintboot进入主页,然后点击learn,选择一个分支,点击Reference Doc进入参考文档页面,在里面可选多页面观看或单页面观看或PDF。在springboot官网上的文档是英文文档,可以搜索对应的中文文档。在参考指南的附录部门,会给出常用应用程序的属性配置选项,可以在
application.properties
文件内,application.yml
文件内或命令行开关中指定各种属性。 - snapshot 快照 在maven中version属性会使用 2.0.2-SNAPSHOT表示是快照版本
- 一般springboot集成其他插件分为两大步:1)添加maven依赖 2)添加配置
redis相关
-
安装路径
/usr/local/redisFormiaosha -
修改的配置
- daem yes
- requirepass 123456 //访问密码 123456
这导致每次使用redis-cli之后,需要先输入auth 123456授权 - bind 0.0.0.0 //任何地址可以访问
-
把redis做成系统服务
- 制作
- 启动 在redis目录下 ./redis-serve ./redis.config
- 停止 使用 service redis_6379 stop 因有密码 无法停止 https://blog.csdn.net/aoshilang2249/article/details/88555828
linux相关
- ps -ef | grep redis
- su 切换用户
- chkconfig --list | grep redis 查看系统服务是否有redis
- 带密码的系统服务启动问题
集成Redis
一般springboot集成其他插件分为两大步:1)添加maven依赖 2)添加配置。3)获取配置 4)生成实例 5)使用。
Redis的常用三个java客户端:Jedis,Redisson,Lettuce。Jedis 和 Lettuce 是 Java 操作 Redis 的客户端。在 Spring Boot 1.x 版本默认使用的是 Jedis ,而在 Spring Boot 2.x 版本默认使用的就是Lettuce。
-
添加依赖
在pom文件中,添加Jedis依赖 -
在application中添加配置信息
spring.redis.host=175.175.16.239 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password=123456 # 链接超时时间 单位 ms(毫秒) spring.redis.timeout=3000 #redispool最大活动对象数 spring.redis.jedis.pool.max-active=100 #redispool当池内没有返回对象时,最大等待时间 #此处按照秒来配置 注意在redispoolconfig对象的参数是毫秒 spring.redis.jedis.pool.max-wait=10 #redispool最大能够保持idel状态的对象数 spring.redis.jedis.pool.max-idle=100
-
创建configuration类,获取配置信息
@Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.jedis.pool.max-active}") private int maxActive; @Value("${spring.redis.jedis.pool.max-idle}") private int maxIdle; @Value("${spring.redis.jedis.pool.max-wait}") private int maxWait;
-
获取jedispool,然后使用jedispool获取jedis实例
jedis = jedisPool.getResource();
-
封装jedis提供的方法。封装主要针对使用redis的封装。
public <T> T get(String key,Class<T> clazz){ Jedis jedis = null; try{ jedis = jedisPool.getResource(); String s = jedis.get(key); T t = stringToBean(s,clazz); return t; }finally { //关闭连接 returnToPool(jedis); } }
添加Fastjson依赖
Fastjson作用可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。对象不可以存储到数据库,而json可以存储到数据库,即Fastjson可以序列化。
序列化最快的是谷歌的protobuf,但是序列化之后不可读(二进制)。
Fastjson序列化之后内容可读(json格式),但是效率是protobuf的一半。
springboot注入bean
* redis的key都是string类型,而value有五种类型。
* 本项目假设所有value均为string类型,因此存在类型转化问题。
* 在存储时,需要将程序产生的java的类型(可能时 int、integer、bean等)转化为string类型,存储到redis中
* 在读取时,需要将从redis中取出的字符串转化为程序可使用的java数据类型;
* 以上的两个过程用到了fastjson的相关方法,具体使用参加get set方法。
Springboot注解
@Component
有一个接口,在这个接口的实现类里,需要用到@Autowired注解,一时大意,没有在实现类上加上@Component注解,导致了Spring报错,找不到这个类。
一旦使用关于Spring的注解出现在类里,例如我在实现类中用到了@Autowired注解,被注解的这个类是从Spring容器中取出来的,那调用的实现类也需要被Spring容器管理,加上@Component
@ConfigurationProperties(prefix = “spring.redis”)
@ConfigurationProperties 是spring-boot中特有的注解,可以使属性文件中的值和类中的属性对应起来。
使用场景如下:
假设application.properties 文件存在redis配置如下:
redis.config.maxTotal=5000
redis.config.maxIdle=10
redis.config.maxWaitMillis=5000
在定义的Redis配置类中
@Configuration
@ConfigurationProperties(prefix = "redis.config")
@Data
public class RedisConfiguration {
private int maxTotal;
private int maxIdle;
private int maxWaitMillis;
...
}
以上代码会将配置文件中的值“赋值”给类属性,当创建RedisConfiguration类时,使用getXX方法即可取到配置文件中的值。RedisConfiguration
实际上是配置文件和调用者之间的桥梁。
这是读取配置一种方式,另外一种方式是使用value注解。
读取配置另外一种方式
使用用@Value注解。格式为@Value("${xx.xx.xx}")
@Value(“${spring.redis.maxTotal}”) //括号里要写全路径
private int maxTotal;
这个直接
@bean
Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中。
@Configuration
读取配置文件,内部可使用@value注解.
@Value读取具体配置值给属性。
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.max-wait}")
private int maxWait;
...
}
java.lang.SuppressWarnings
可以标注在类、字段、方法、参数、构造方法,以及局部变量上。
作用:告诉编译器忽略指定的警告,不用在编译完成后出现警告信息。
@ControllerAdvice
@ControllerAdvice注解是Spring3.2中新增的注解,学名是Controller增强器,作用是给Controller控制器添加统一的操作或处理。
该注解作用对象为TYPE,包括类、接口和枚举等,在运行时有效,并且可以通过Spring扫描为bean组件。其可以包含由@ExceptionHandler、@InitBinder 和@ModelAttribute标注的方法,可以处理多个Controller类,这样所有控制器的异常可以在一个地方进行处理。
增强controler的意思是指在一般controller的基础上【拦截特定的请求】,拦截所有异常,功能更强大。
对于@ControllerAdvice,我们比较熟知的用法是结合@ExceptionHandler用于全局异常的处理。
@ExceptionHandler
用于拦截异常。当一个Controller中有方法加了@ExceptionHandler之后,这个Controller其他方法中没有捕获的异常就会以参数的形式传入加了@ExceptionHandler注解的那个方法中。
如果单使用@ExceptionHandler,只能在当前Controller中处理异常。但当配合@ControllerAdvice一起使用的时候,就可以摆脱那个限制,对所有controller层异常进行处理,即所谓的全局异常处理。此时可以在全局异常拦截器中给ExceptionHandler注解指定要拦截的异常类型。
//ExceptionHandler配合ControllerAdvice,在全局拦截器中定义针对多个不同类的拦截器。
@ControllerAdvice
@ResponseBody
public class WebExceptionHandle {
xxx
//拦截器1 拦截HttpRequestMethodNotSupportedException中的异常
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ServiceResponse handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
xxx
}
//拦截器2 拦截HttpMediaTypeNotSupportedException中的异常
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ServiceResponse handleHttpMediaTypeNotSupportedException(Exception e) {
xxx
}
xxx
}
@Restcontroller @Controller @Controller + @ResponseBody
@Controller
单据使用@Controller不加@ResponseBody,用于返回一个视图,该情况属于比较传统的Spring MVC应用,前后端不分离的场景。
Spring回去resources/templates目录下查找hello.html,并且携带参数
@Controller
public class HelloController {
@GetMapping("/hello")
public String greeting(@RequestParam(name = "name", required = false, defaultValue = "World") String name, Model model) {
model.addAttribute("name", name);
return "hello";
}
}
@RestController
只返回对象,并且对象直接以JSON或XML的形式传入HTTP响应中,该情况属于RESTful Web服务,前后端分离场景。
@Controller
public class HelloController {
@PostMapping("/hello")
@ResponseBody
public Person greeting(@RequestBody Person person) {
//返回json格式的Person对象
return person;
}
@Controller + @ResponseBody
返回JSON或XML形式数据;在Spring4之前开发RESTful Web服务,需要使用@Controller + @ResponseBody 注解使用;在Spring4之后,使用@RestController即可
//@Controller + @ResponseBody在功能层面上等于@RestController
@RestController
public class HelloController {
@PostMapping("/hello")
public Person greeting(@RequestBody Person person) {
//返回json格式的Person对象
return person;
}
}
设计模式
- 模板模式 在设计redis的key阶段
FastJson
fastjson 是一个性能很好的 Java 语言实现的 JSON 解析器和生成器,来自阿里巴巴的工程师开发。其主要特点是:
① 快速:fastjson采用独创的算法,将parse的速度提升到极致,超过所有基于Java的json库,包括曾经号称最快的jackson;
② 强大:Fastjson完全支持https://json.org的标准(也是Google官方网站收录的参考实现之一);支持各种JDK类型;包括基本类型、JavaBean、Collection、Map、Enum、泛型等;
③零依赖:没有依赖其它任何类库除了JDK,能够直接运行在Java SE 5.0以上版本;支持Android;开源 (Apache 2.0)。
parseObject(String str)
该方法将str转化为相应的JSONObject对象,其中str是“键值对”形式的json字符串,转化为JSONObject对象之后就可以使用其内置的方法,进行各种处理。
JSON和JSONObject都提供该方法,且JSONObject是JSON的子类,当调用JSONObject.parseObject(result)时,会直接调用父类的parseObject(String text)。一个是用父类去调用父类自己的静态的parseObject(String text),一个是用子类去调用父类的静态parseObject(String text),两者调的是同一个方法。
遇到的问题
1、在idea中,使用jedis操作vmware虚拟机中的redis出现java.net.SocketException: Network is unreachable: connect
异常。这个问题主要是本机和虚拟机无法互通。
最终原因有两个
-
RedisConfig获取host的注解写错,将以下value中的host写成port
-
@Value("${spring.redis.host}") private String host;
-
主机和虚拟机无法互通,这需要关闭主机和虚拟机的防火墙,注意是两者的防火墙都需要关闭。
-
redis所在虚拟机的网络设置为桥接模式,并设置centos的ip和主机中虚拟机对应的网卡的ipv4都设置为自动获取。
日志使用
第一步 在类中定义一个静态日志对象,有不同的方式
第二步 在想打印的内容输出在idea控制台,方便调试
public class LoginController {
//定义日志对象
private static Logger log = LoggerFactory.getLogger(LoginController.class);
@RequestMapping("/to_login")
public String home(){
return "login";
}
@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> dologin(LoginVo loginVo){
//打印在idea的控制台
log.info(loginVo.toString());
return null;
}
}
Vo
value object 值对象 / view object 表现层对象.定义在后端的实体,用于接收前端数据,
接收前端传递给后端的值:前端的ajax请求的data(是个json对象)中有两个属性,后端创建一个对象包含这些属性,然后就可以接收前端的值。
登录的对象 LoginVo接收登录页面传递的实体。
HttpServletRequest&HttpServletResponse
前端请求controller中的方法,任一方法中都可以使用HttpServletRequest,该类用于存储请求参数。
HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,通过它可以获得客户的所有参数信息。
Web服务器收到一个http请求,会针对每个请求创建一个HttpServletRequest和HttpServletResponse对象,从客户端取数据找HttpServletRequest,向客户端发送数据就是HttpServletResponse,HttpServletResponse对象可以向客户端发送三种类型的数据:a.响应头b.状态码c.数据.
这两个参数一般用在controller的方法中,用于后端获取当前请求的参数值或后端向当前请求中设置参数值。
使用Springboot,我们很多时候直接使用@PathVariable、@RequestParam、@Param来获取较少数量的参数值,而HttpServletRequest可以获取全部。
异常
RuntimeException是程序错误,需要修改程序,自然不需要抛出异常。Error是jvm错误,较严重,程序员无可奈何,抛出异常没有意义。这两项是非受检异常(免检,编译器不检查 程序是否处理这两类异常)
对于受检异常,可认为是导致程序中断的一种可能情况,程序员需要手动处理这种情况。
throw throws
泛型方法
其他知识
-
抽象类
抽象类不能用来实例化对象,声明抽象类的唯一目的是为了将来对该类进行扩充。如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类。
抽象类可以避免被new,只能通过子类创建。
-
访问修饰符
- private 和 public 两极分化,前者只能在类内部访问,后者在所有位置可访问
- default 包访问权限,只要在一个包下都可以访问,也是默认访问修饰符
- protected 包和子类可以访问,有特殊情况,略去。
-
由普通的类来实现接口,必须将接口所有抽象方法重写
由抽象类来实现接口,则不必重写接口的方法。可以全部不重写或只重写一部分方法。
需求分析
环境搭建
- 整合redis,设计生成key的方式,用到了模板方法模式
登录模块
密码保护策略
进行两次MD5操作,双重保护密码的安全。
-
用户端在输入密码之后,使用MD5进行加密,避免密码以明文的方式在网络上传输。即`PSW1 = MD5(明文 + 固定Salt)
-
服务端在接收到用户密码之后,再次使用MD5加密,然后存储到数据库,避免密码数据库被盗。即`PSW2 = MD5(PSW1 + 随机Salt)。
-
salt的使用方式。将salt定义为静态变量,使用其中的某个字符。
//固定的salt,前端也使用这个salt private static final String salt = "1a2b3c4d"; md5("" + salt.charAt(1) + salt.charAt(2) + psw + salt.charAt(4))
JSR303参数校验
JSR303 规范(Bean Validation 规范)提供了对 Java EE 和 Java SE 中的 Java Bean 进行验证的方式。该规范主要使用注解的方式来实现对 Java Bean 的验证功能,并且这种方式会覆盖使用 XML 形式的验证描述符,从而使验证逻辑从业务代码中分离出来。
在对参数设置值时,触发校验规则。可用于校验前端页面向后端传递的值,后端新建vo接收该值。
1.引入pom,即可使用
不引入import javax.validation.constraints.NotNull;
2.对于常规的校验规则,譬如长度、非空等,直接在实体的属性上添加注解即可,这些有校验框架内部定义完成的。
public class LoginVo {
@NotNull //常规校验 直接添加注解
@Length(min=6,max=50)
private String password;
...
}
3.对于特殊的校验规则,譬如手机号码格式,需要自定义
public class LoginVo {
@NotNull
@IsMobile //特殊校验 需要自定义
private String mobile;
...
}
自定义校验规则要写两项内容
//其一
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@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 { };
}
//其二 具体的校验逻辑
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);
}
}
}
}
-
使用valid注解
@Valid注解可以实现数据的验证,你可以定义实体,在实体的属性上添加校验规则,而在API接收数据时添加@valid关键字,这时你的实体将会开启一个校验的功能。
@RequestMapping("/do_login") @ResponseBody public Result<Boolean> dologin(@Valid LoginVo loginVo){ //valid会替代以下注释中的校验逻辑 /*String mobile = loginVo.getMobile(); String password = loginVo.getPassword(); if(mobile == null){ return Result.error(CodeMsg.MOBIL_EMPTY); } if(password == null){ return Result.error(CodeMsg.PASSWORD_EMPTY); } if(!ValidatorUtil.isMobile(mobile)){ return Result.error(CodeMsg.MOBIL_ERROR); }*/ log.info(loginVo.toString()); CodeMsg login = miaoshaUserService.login(loginVo); log.info(login.toString()); if(login.getCode() == 0 ){ return Result.success(true); } return Result.error(login);//? }