目录
一、缓存优化
1.1 页面缓存
将不经常改动的页面直接缓存到redis中,然后用Thymeleaf视图解析器将缓存的页面直接渲染出来。
1.2 对象缓存
将经常使用的对象信息放入redis中,比如说用户信息,抽插redis肯定比抽插数据库快。但是,这里面就涉及到一个数据同步问题,即如何保持redis中放入的是最新的数据。策略就是遇到数据更新的时候,先更新数据库中的信息,然后使缓存失效,当再次拉取数据的时候就会从数据库中获取,第一次获取成功后就放入缓存当中。
1.3 页面静态化
将页面直接缓存到用户的浏览器上,或者将页面直接转化为静态网页。静态化是指把动态生成的HTML页面变为静态内容保存,以后用户的请求到来,直接访问静态页面,不再经过服务的渲染。而静态的HTML页面可以部署在nginx中,从而大大提高并发能力,减小tomcat压力。通过Thymeleaf模板引擎来生成静态网页。
1.4 静态资源优化
js/css压缩,减少流量;多个js/css组合,减少连接数;CDN优化。
二、数据库优化
减少对数据库的访问,可以提高性能。秒杀时因为有大量用户进行下订单操作,所有可以使用消息队列来缓解数据库压力。同时也可以想办法优化对redis的访问,设置内存标记等。
三、安全方面相关的优化
3.1 秒杀地址隐藏
功能:防止秒杀地址被刷。
思路:秒杀开始之前,先去请求接口获取秒杀地址
- 改造接口,带上PathVariable参数
- 添加生成地址的接口
- 秒杀收到请求,先验证PathVariable
3.1.1 创建秒杀路径
Controller
/**
* 创建秒杀路径
* @param goodsId
* @return
*/
@GetMapping("get_path/{goodsId}")
public ResponseEntity<String> getSeckillPath(@PathVariable("goodsId") Long goodsId){
UserInfo userInfo = LoginInterceptor.getLoginUser();
if (userInfo == null){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
String str = this.seckillService.createPath(goodsId,userInfo.getId());
if (StringUtils.isEmpty(str)){
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(str);
}
Service
将用户id和秒杀商品的id先进行加密,然后放入redis中,并且设置过期时间为60秒。
/**
* 创建秒杀地址
* @param goodsId
* @param id
* @return
*/
@Override
public String createPath(Long goodsId, Long id) {
String str = new BCryptPasswordEncoder().encode(goodsId.toString()+id);
BoundHashOperations<String,Object,Object> hashOperations = this.stringRedisTemplate.boundHashOps(KEY_PREFIX_PATH);
String key = id.toString() + "_" + goodsId;
hashOperations.put(key,str);
hashOperations.expire(60, TimeUnit.SECONDS);
return str;
}
3.1.2 路径验证
改造创建秒杀订单接口,让其先验证路径
Controller
Service
/**
* 验证秒杀地址
* @param goodsId
* @param id
* @param path
* @return
*/
@Override
public boolean checkSeckillPath(Long goodsId, Long id, String path) {
String key = id.toString() + "_" + goodsId;
BoundHashOperations<String,Object,Object> hashOperations = this.stringRedisTemplate.boundHashOps(KEY_PREFIX_PATH);
String encodePath = (String) hashOperations.get(key);
return new BCryptPasswordEncoder().matches(path,encodePath);
}
3.2 接口限流
功能:限定用户在某一段时间内有限次的访问地址。
思路:将用户访问地址的次数写入redis当中,同时设置过期时间。当用户每次访问,该值就加一,当访问次数超出限定数值时,那么就直接返回。
实现:为了具有通用性,以注解的形式调用该方法。
3.2.1 定义注解
package com.leyou.seckill.access;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Author: 98050
* @Time: 2018-11-23 23:38
* @Feature: 接口限流注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
/**
* 限流时间
* @return
*/
int seconds();
/**
* 最大请求次数
* @return
*/
int maxCount();
/**
* 是否需要登录
* @return
*/
boolean needLogin() default true;
}
3.2.2 添加拦截器
通过拦截器,拦截AccessLimit注解,然后进行接口限流。主要是使用redis的自增机制。
package com.leyou.seckill.interceptor;
import com.leyou.auth.entity.UserInfo;
import com.leyou.seckill.access.AccessLimit;
import com.leyou.utils.JsonUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.TimeUnit;
/**
* @Author: 98050
* @Time: 2018-11-23 23:45
* @Feature: 接口限流拦截器
*/
@Service
public class AccessInterceptor extends HandlerInterceptorAdapter {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
if (accessLimit == null){
return true;
}
//获取用户信息
UserInfo userInfo = LoginInterceptor.getLoginUser();
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if (needLogin){
if (userInfo == null){
render(response, "用户没有登录");
return false;
}
key += "_" + userInfo.getId();
}else {
//不需要登录,则什么也不做
}
String count = redisTemplate.opsForValue().get(key);
if (count == null){
redisTemplate.opsForValue().set(key,"1",seconds, TimeUnit.SECONDS);
}else if(Integer.valueOf(count) < maxCount){
redisTemplate.opsForValue().increment(key,1);
}else {
render(response,"稍后再试");
}
}
return super.preHandle(request, response, handler);
}
private void render(HttpServletResponse response, String str) throws IOException {
OutputStream outputStream = response.getOutputStream();
outputStream.write(str.getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
配置拦截器
3.2.3 使用
在需要限流的方法上,直接使用注解即可。
四、相关知识
4.1 微服务架构如何保障高可用
- 首先你的hystrix资源隔离以及超时这块,必须设置合理的参数,避免高峰期,频繁的hystrix线程卡死
如何设置Hystrix线程池大小
假设你的服务A,每秒钟会接收30个请求,同时会向服务B发起30个请求,然后每个请求的响应时长经验值大概在200ms,那么你的hystrix线程池需要多少个线程呢?
计算公式是:30(每秒请求数量) * 0.2(每个请求的处理秒数) + 4(给点缓冲buffer) = 10(线程数量)。
如果对上述公式存在疑问,不妨反过来推算一下,为什么10个线程可以轻松抗住每秒30个请求?
一个线程200毫秒可以执行完一个请求,那么一个线程1秒可以执行5个请求,理论上,只要6个线程,每秒就可以执行30个请求。
也就是说,线程里的10个线程中,就6个线程足以抗住每秒30个请求了。剩下4个线程都在玩儿,空闲着。
那为啥要多搞4个线程呢?很简单,因为你要留一点buffer空间。
万一在系统高峰期,系统性能略有下降,此时不少请求都耗费了300多毫秒才执行完,那么一个线程每秒只能处理3个请求了,10个线程刚刚好勉强可以hold住每秒30个请求。所以你必须多考虑留几个线程。
如何设置请求超时时间
接着来,那么请求的超时时间设置为多少?答案是300毫秒。
如果你的超时时间设置成了500毫秒,想想可能会有什么后果?
考虑极端情况,如果服务B响应变慢,要500毫秒才响应,你一个线程每秒最多只能处理2个请求了,10个线程只能处理20个请求。
- 其次,针对个别的服务故障,要设置合理的降级策略,保证各个服务挂了,可以合理的降级,系统整体可用!
如果你的某个服务挂了,那么你的hystrix会走熔断器,然后就会降级,你需要考虑到各个服务的降级逻辑。
举一些常见的例子:
- 如果查询数据的服务挂了,你可以查本地的缓存
- 如果写入数据的服务挂了,你可以先把这个写入操作记录日志到比如mysql里,或者写入MQ里,后面再慢慢恢复
- 如果redis挂了,你可以查mysql
- 如果mysql挂了,你可以把操作日志记录到es里去,后面再慢慢恢复数据。
具体用什么降级策略,要根据业务来定,不是一成不变的。
4.2