Java秒杀系统

目录

第一章 项目框架搭建 

第二章 实现登录功能

数据库设计

明文密码两次MD5处理

JSR303参数校验+全局异常处理器

分布式session

第三章 实现秒杀功能

数据库设计

商品列表页

商品详情页

秒杀功能实现

订单详情页

第四章 JMeter压测

JMeter入门

自定义变量模拟多用户

JMeter命令行使用

Redis的压测工具redis-benchmark

Spring Boot打war包

第五章 页面优化技术

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

页面静态化,前后端分离

静态资源优化

CDN优化

解决超卖问题

第六章 接口优化

Redis预减库存减少数据库的访问

内存标记减少Redis访问

系统初始化时,把商品的库存加载到Redis

请求先入队缓冲,异步下单,增强用户体验

Nginx水平扩展

压测

第七章 安全优化

秒杀接口地址隐藏

数学公式验证码

接口的限流防刷

第八章 总结


第一章 项目框架搭建 

  • 1 Spring Boot环境搭建
  • 2 集成Thymeleaf,Result结果封装
  • 3 集成mybatis+Druid

导入启动项,在application.properties写入相关的配置。可以使用声明式事务控制

  • 4 集成Jedis+Redis安装+通用缓存key封装

安装Redis

启动Redis:在cmd下切到redis目录:cd C:\Program Files\Redis-x64-3.0.504然后使用命令:redis-server redis.windows.conf

在另一个cmd下使用redis-cli来使用redis

修改配置文件

哪台机器能访问这个redis,因为做分布式所以要设置所有机器能访问

设置密码

Java秒杀系统_第1张图片

可以将redis加入系统的服务(加不进去不知道为啥,每次用手动启动)

Redis和springboot集成:

依赖:

添加Jedis依赖

添加Fastjson依赖:序列化时将java对象转化成json字符串写到redis服务器中(序列化后可读,方便查看)

配置:

通过ConfigurationProperties注解可以将配置文件有关redis的配置项读取进来并赋值给相应属性

将代表Redis配置信息的RedisConfig的类通过@Component添加到容器中

通过JedisPool获取Jedis对象,通过Jedis对Redis进行操作,在插入数据的时候,将bean转换为string插入,获取数据的时候将str转换为对应的bean(使用fastjson)

出现的问题:多个人向redis中插入的数据key相同会将数据覆盖?

解决思路:在进行插入数据的时候加入前缀,比如用户数据表,前缀就为USER(前缀可通过插入数据类型的类名获得)

模板模型:接口定义需要实现的功能,抽象类定义通用方法,实现类实现特有的方法.

通过KeyPrefix类来生成带有前缀的key,最终的key为:类名+指定的prefix+原来的key

第二章 实现登录功能

数据库设计

Java秒杀系统_第2张图片

明文密码两次MD5处理

Java秒杀系统_第3张图片

用户端passMD5加密后传入服务端,服务端得到用户端传来的pass再进行MD5存入数据库。

第一次MD5防止用户密码在网络上传输不安全,第二次MD5若数据库被盗防止被反查找出密码。

静态文件

Java秒杀系统_第4张图片

bootstrap画页面

jquery-validation错误表单验证

layer做弹框

md5

jquery

common应用自己的

登录功能实现:使用正则表达式判断mobile是否格式正确.用户输入密码时,先进行MD5

加固定盐值的加密再进行传输,传到服务端后,服务端根据mobile获取用户信息,包括password和salt,取这个salt对这个密码再进行一次MD5加密,最终和数据库的password比较,若相同登录成功,若不同,登陆失败.

JSR303参数校验+全局异常处理器

JSR 303 用于对Java Bean 中的字段的值进行验证,使得验证逻辑从业务代码中脱离出来

登录时参数校验

现在需要校验的参数上加@Valid标签

然后在需要校验对象的属性中:

Java秒杀系统_第5张图片

可以使用自带的注解进行参数校验,也可以自己写参数校验规则,如@IsMobile

全部异常处理:

拦截异常以更友好的形式呈现而不是exception的堆栈,捕捉到异常放回Result包装codemsg。在进行登陆的时候,若验证不成功,直接抛异常,将异常交给全局异常来进行处理,代码更加简洁

/**
 * 在Spring 3.2中,新增了@ControllerAdvice、@RestControllerAdvice 注解,可以用于定义@ExceptionHandler、
 * @InitBinder、@ModelAttribute,并应用到所有@RequestMapping、@PostMapping, @GetMapping注解中。
 * 可以将异常拦截进行处理
 */
@ControllerAdvice
@ResponseBody
public class GlobleExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public Result exceptionHandler(HttpServletRequest request,Exception e){
        if(e instanceof BindException){
            BindException ex = (BindException) e;
            List error = ex.getAllErrors();
            ObjectError error1 = error.get(0);
            String msg = error1.getDefaultMessage();
            return Result.error(CodeMsg.BIND_ERROR.fillArgs());
        }else{
            return Result.error(CodeMsg.SERVER_ERROR);
        }
    }
}

分布式session

//Token是一个字符串,是一段根据特殊规则生成的唯一的、保存在缓存服务器(如Redis)中的key,这个key对应的value是用户账户
// 数据。每个用户登录后,服务器生成一个类似Token并缓存,之后用户每次请求中都要带上这个Token,以便实现类似
// 于HTTP Session的会话跟踪。

把session信息存放到第三方缓存中

在第一次登录的时候,生成一个随机的token,将token:user存入redis中,同时将token存入cookie中返回给客户端,客户端下次请求的时候携带上这个token就可以获取用户信息。将seesion存入缓存中。

问题:每次生成新的token存入redis,怎么把旧的删除

WebMvcConfigurerAdapter

分布式session相关

第三章 实现秒杀功能

数据库设计

Java秒杀系统_第6张图片

设置单独的秒杀商品表和秒杀订单表而不是在商品或订单后面加一个标识标识其为秒杀商品:活动很多难以维护

Java秒杀系统_第7张图片

 问题:使用mybatis generator逆向工程创建pojo和dao及相应的xml

商品列表页

查询商品秒杀商品列表

sql的左连接

@Select("select g.*,mg.stock_count,mg.start_date,mg.end_date from miaosha_goods mg left join goods g on mg.goods_id=g.id")
public List ListGoodsVo();

改进点:价格用decimal来表示,浮点数精度丢失 

商品详情页

封装GoodsVO过去goods信息和miaoshagoods信息,然后给model传递到前端,同时秒杀状态和秒杀倒计时也通过model传递到前端。

秒杀倒计时功能

config包下面的代码

思考不通过thmpeleaf测试功能,转发

秒杀功能实现

查是否登录,未登录返回登录页面,查是否已经秒杀过,一个用户只能秒杀一次,已经秒杀过返回异常页面

减库存,生成order_info 生成miaoshao_order(必须是事务)

订单详情页

第四章 JMeter压测

JMeter入门

下载解压直接点击bin目录下的jmeter.bat即可启动(图形界面)

每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。

添加线程组

添加HTTP请求的默认值

添加sample-HTTP

查看结果:监听器-聚合报告

没有优化之前,对于goods/to_list接口,10000的请求量,qps为304.2

 查了一次数据库,瓶颈应该在数据库上

 @RequestMapping("/to_list")
    public String list(Model model, MiaoshaUser user) {
//        if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){
//            return "login";
//        }
//        String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
//        MiaoshaUser miaoshaUser = userService.getByToken(response,token);
        model.addAttribute("user", user);
        List goodsList = goodsService.listGoodsVo();
        model.addAttribute("goodsList",goodsList);
        return "goodslist";
    }

资源监听器:perfmon.msc 瓶颈在数据库 

自定义变量模拟多用户

压测:user/info接口,通过token获取用户信息接口。带参数的请求,参数为token,10000的请求量,qps为614.3

Java秒杀系统_第8张图片

 

后面的方法QPS高,因为获取用户信息的时候读了Redis缓存

上述是在参数为同一个进行压测

需要参数为不同时:

Java秒杀系统_第9张图片

需要传出代表参数的文件

Java秒杀系统_第10张图片

引用:${UserToken}

Java秒杀系统_第11张图片

JMeter命令行使用

Java秒杀系统_第12张图片

Redis的压测工具redis-benchmark

Redis本身自带的压测工具redis-benchmark。

压测需要一段时间,因为它需要依次压测多个命令的结果,如:get、set、incr、lpush等等

-c 100个并发连接处理100000条请求,需要50.92秒(每秒1963.83条命令)(以三字节传输)

redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000

Java秒杀系统_第13张图片

存储大小为100字节的数据包(以100字节传输)-q只做简单的输出

redis-benchmark -h 127.0.0.1 -p 6379 -q -d 100

Java秒杀系统_第14张图片

只测试某些操作的性能,-t只测试指定的操作

redis-benchmark -t set,get -n 100000 -q

只测试某些数值存取的性能

redis-benchmark -n 100000 -q script load "redis.call('set','foo','bar')"

Spring Boot打war包

war包和jar包的区别

Java秒杀系统_第15张图片

Java秒杀系统_第16张图片

Java秒杀系统_第17张图片

压测to_list接口,50000条请求,后面以此基准做优化,QPS为325.2/sec

主要的接口是miaosha/do_miaosha,这个接口做压测

先生成5000条MiaoshaUser数据插入数据库,然后登录获得每一个用户的token存入redis

生成token文件(一共5000条记录)

Java秒杀系统_第18张图片

然后配置Jmeter

Java秒杀系统_第19张图片

Java秒杀系统_第20张图片

多线程会出现超卖问题,库存变为负数

第五章 页面优化技术

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

页面缓存:先从缓存中找,找到就直接返回,找不到再进行渲染,然后缓存到客户端。(页面缓存一般有效期较短)(为了防止一个时间段大量访问才使用)

  1. 取缓存
  2. 手动渲染模板:使用thymeleafViewResolver
  3. 结果输出:直接返回html页面
/**
     * 直接返回html
     */
    @RequestMapping(value = "/to_list",produces = "text/html")
    @ResponseBody
    public String list(HttpServletRequest request,HttpServletResponse response,Model model, MiaoshaUser user) {
//        if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){
//            return "login";
//        }
//        String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
//        MiaoshaUser miaoshaUser = userService.getByToken(response,token);
        model.addAttribute("user", user);
        List goodsList = goodsService.listGoodsVo();
        model.addAttribute("goodsList",goodsList);
        String html = redisService.get(GoodsKey.getGoodsList,"",String.class);
        if(!StringUtils.isEmpty(html)){
            return html;
        }
        //手动渲染,存入缓存
        IWebContext ctx = new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());
        html = thymeleafViewResolver.getTemplateEngine().process("goodslist",ctx);
        if(!StringUtils.isEmpty(html)){
            //保存到缓存中
            redisService.set(GoodsKey.getGoodsList,"",html);
        }
        return html;
    }

 URL缓存:商品详情页面

对象缓存(更细粒度的缓存):通过id获取到用户之后存入缓存,下次可以直接从缓存中取,通过token获取用户也是对象缓存

public MiaoshaUser getById(long id){
        //取缓存
        MiaoshaUser miaoshaUser = redisService.get(MiaoshaUserKey.getById,""+id,MiaoshaUser.class);
        if(miaoshaUser!=null){
            return miaoshaUser;
        }
        //取数据库
        miaoshaUser = miaoshaUserDao.getById(id);
        if(miaoshaUser!=null){
            //存入缓存
            redisService.set(MiaoshaUserKey.getById,""+id,MiaoshaUser.class);
        }
        return miaoshaUser;
}

页面静态化,前后端分离

将页面缓存到用户的浏览器上

  • 常用技术AngularJS、Vue.js
  • 优点:利用浏览器的缓存

静态资源优化

CDN优化

解决超卖问题

@Update("update miaosha_goods set stock_count=stock_count-1 where goods_id=#{goodsId} and stock_count>0")
    public int reduceStock(MiaoshaGoods miaoshaGoods);

在更新库存的时候,判断库存是否大于0,大于0才进行更新

解决一个用户同时可以秒杀两个商品:在创建秒杀订单的时候会创建另个,一个是秒杀订单,一个是订单详情.将秒杀订单的userid设置为唯一索引,那么第二条记录就不能插入.

Java秒杀系统_第21张图片

@RequestMapping("/do_miaosha")
    @ResponseBody
    public Result miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
        model.addAttribute("user",user);
        if(user==null){
            return Result.error(CodeMsg.SESSION_ERROR);
        }
        //判断库存
        GoodsVo goods = goodsDao.getGoodsVoByGoodsId(goodsId);
        int stock = goods.getGoodsStock();
        if(stock<=0){
            model.addAttribute("errmsg", CodeMsg.MIAO_SHA_OVER.getMsg());
            return Result.error(CodeMsg.MIAOSHA_FAIL);
        }
        //判断是否已经秒杀到(一个人只能秒杀一件商品)
        MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
        if(miaoshaOrder!=null){
            model.addAttribute("errmsg",CodeMsg.REPEATE_MIAOSHA.getMsg());
            return Result.error(CodeMsg.MIAOSHA_FAIL);
        }
        //减库存 下订单 写入秒杀订单(必须是事务)
        OrderInfo orderInfo = miaoshaService.miaosha(user,goods);
        model.addAttribute("orderInfo",orderInfo);
        model.addAttribute("goods",goods);
        return Result.success(orderInfo);
}

优化1:判断是否秒杀到,查是否有已经秒少到商品的时候,查缓存,若有直接返回错误,没有就查数据库,然后再写入缓存中.

第六章 接口优化

思路:减少数据库的访问

Redis预减库存减少数据库的访问

  1. 系统初始化时,把商品的库存加载到Redis
  2. 收到请求,Redis预减库存,库存不足,直接返回,否则进入3
  3. 请求入队,立即返回排队中
  4. 请求出队,生成订单,减少库存
  5. 客户端轮询,是否秒杀成功

RabbitMq安装

https://www.cnblogs.com/vaiyanzi/p/9531607.html

解决中文路径问题

https://www.cnblogs.com/bade/p/10303687.html

SpringBoot集成RabbitMQ

  1. 添加依赖,写配置文件
  2. 创建消息接受者
  3. 创建消息发送者

配置文件 

#rabbitmq
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
#\u6D88\u8D39\u8005\u6570\u91CF
#消费者数量
spring.rabbitmq.listener.simple.concurrency= 10
#最大消费者数量
spring.rabbitmq.listener.simple.max-concurrency= 10
#\u6D88\u8D39\u8005\u6BCF\u6B21\u4ECE\u961F\u5217\u83B7\u53D6\u7684\u6D88\u606F\u6570\u91CF
#消费者每次从队列获取的消息数量。写多了,如果长时间得不到消费,数据就一直得不到处理
spring.rabbitmq.listener.simple.prefetch= 1
#\u6D88\u8D39\u8005\u81EA\u52A8\u542F\u52A8
#消费者自动启动
spring.rabbitmq.listener.simple.auto-startup=true
#\u6D88\u8D39\u5931\u8D25\uFF0C\u81EA\u52A8\u91CD\u65B0\u5165\u961F
#消费者消费失败,自动重新入队
spring.rabbitmq.listener.simple.default-requeue-rejected= true
#\u542F\u7528\u53D1\u9001\u91CD\u8BD5
#启用发送重试 队列满了发不进去时启动重试
spring.rabbitmq.template.retry.enabled=true 
#1秒钟后重试一次
spring.rabbitmq.template.retry.initial-interval=1000
#最大重试次数 3次
spring.rabbitmq.template.retry.max-attempts=3
#最大间隔 10秒钟
spring.rabbitmq.template.retry.max-interval=10000
#等待间隔 的倍数。如果为2  第一次 乘以2 等1秒, 第二次 乘以2 等2秒 ,第三次 乘以2 等4秒
spring.rabbitmq.template.retry.multiplier=1.0

 rabbitmq的四种交换机模式

内存标记减少Redis访问

  1. 系统初始化时,把商品的库存加载到Redis
  2. 收到请求,Redis预减库存,库存不足,直接返回,将消息加入消息队列中

系统初始化时,把商品的库存加载到Redis

实现InitializingBean接口,重写afterPropertiesSet()方法,这个方法将在系统初始化的时候调用.

    @Override
    public void afterPropertiesSet() throws Exception {
        List goodsVoList = goodsService.listGoodsVo();
        if(goodsVoList==null){
            return;
        }
        for(GoodsVo goods:goodsVoList){
            redisService.set(GoodsKey.getMiaoshaGoodsStock,""+goods.getId(),goods.getStockCount());
        }
    }

使用一个map存储商品是否秒杀完毕,map里面存存goodid:false/true,标记该商品是否秒杀完毕。访问redis之前先判断map中的值,防止一直访问redis。当redis中的库存小于0时,将该商品标记为秒杀完毕。

请求先入队缓冲,异步下单,增强用户体验

  1. 请求入队,立即返回排队中
  2. 请求出队,生成订单,减少库存
  3. 客户端轮询,是否秒杀成功

Nginx水平扩展

压测

第七章 安全优化

秒杀接口地址隐藏

思路:秒杀开始之前,先去请求接口获取秒杀地址

  1. 接口改造,带上PathVariable参数
  2. 添加生成地址的接口
  3. 秒杀收到请求,先验证PathVarible

获取秒杀的path,将path存入redis之中,秒杀的时候验证path

数学公式验证码

思路:点击秒杀之前,先输入验证码,分散用户请求

Java秒杀系统_第22张图片

接口的限流防刷

对接口做限流

第八章 总结

Java秒杀系统_第23张图片

Java秒杀系统_第24张图片

Java秒杀系统_第25张图片

Java秒杀系统_第26张图片

Java秒杀系统_第27张图片

Java秒杀系统_第28张图片

Java秒杀系统_第29张图片

Java秒杀系统_第30张图片

 

你可能感兴趣的:(Spring框架,项目)