乐优商城(四十七)秒杀总结

目录

一、缓存优化

1.1 页面缓存

1.2 对象缓存

1.3 页面静态化

1.4 静态资源优化

二、数据库优化

三、安全方面相关的优化

3.1 秒杀地址隐藏

3.1.1 创建秒杀路径

3.1.2 路径验证

3.2 接口限流

3.2.1 定义注解

3.2.2 添加拦截器

3.2.3 使用

四、相关知识

4.1 微服务架构如何保障高可用


一、缓存优化

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值