高并发秒杀系统

项目环境搭建

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nn18HsrO-1664517455146)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\目标.png)]

导入基础包:spring web, lombok, thymeleaf, mybatis-puls, mysqlDriver

<dependencies>
        <!--thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--mybatis-plus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
    </dependencies>

登录

数据表建立 id nickname password

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z6w9vPoq-1664517455147)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\数据库.png)]

数据源配置

#配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/seckill
spring.datasource.username=root
spring.datasource.password=123456

添加sping-boot-security依赖

<!--ctrl+shift+'/'为快捷注释,spring-boot-security依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置spring-security

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/**").hasRole("user")
                .and()
                .formLogin();
    }
}

用户实体

@Data
public class User {
    int id;
    String nickname;
    String password;
    String roles;
}

根据name查询用户的mapper

@Mapper
public interface SelectUserByName {
    @Select("select * from user where name=#{username}")
    UserData SelectUserByName(String username);
}

用户验证service

@Service
public class UserAuthService implements UserDetailsService {
    @Resource
    SelectUserByName selectUserByName;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserData userData = selectUserByName.SelectUserByName(username);
        //不存在用户
        if(userData==null) throw new UsernameNotFoundException("未找到用户!");
        //存在用户,则返回User(security.core.userdetails.User)形式用户
        return User
                .withUsername(userData.getNickname())
                .password(userData.getPassword())
                .roles(userData.getRoles())
                .build();
    }
}

配置Spring-security中传入参数为AuthenticationManagerBuilder auth的configure。BCryptPasswordEncoder()是spring中一种加密方式,主要计算hash值,该过程不可逆。只有将前端获取的密码进行加密,与使用相同方法加密存储在数据库的hash值匹配。

@Resource
    UserAuthService userAuthService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userAuthService)
                .passwordEncoder(new BCryptPasswordEncoder());//会对输入的密码进行加密,与数据库中的密码匹配,因此存入的密码也应是相同加密后的密码
    }

配置静态文件路径

#配置静态资源
spring.mvc.static-path-pattern=/static/**

开放静态资源权限

.antMatchers("/static/**").permitAll()

关闭crsf(跨站域名伪造)

.csrf().disable();

完成登录映射

<form action="/doLogin" method="post">
.loginProcessingUrl("/doLogin").permitAll()

分布式会话

cookie和session原理:cookie是保存在浏览器的key-value,特点是每次请求cookie会发送给服务端。当浏览器被黑后,cookie中保存的数据便会泄露。session是第一次用户名、密码验证成功后保存在客户端的会话,具有唯一的sessionid及其属性,访问后会将sessionid及其属性存为cookie。

秒杀页面

商品列表

数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3HQeyQS-1664517455147)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\商品数据库.png)]

商品实体类

@Data
public class Goods {
    int id;
    String goods_name;
    int price;
    int stock;
    Date begin_time;
    Date end_time;
}

mapper

@Mapper
public interface SelectGoods {
    @Results({
            @Result(column = "id", property = "id"),
            @Result(column = "goods_name", property = "goods_name"),
            @Result(column = "price", property = "price"),
            @Result(column = "stock", property = "stock"),
            @Result(column = "begin_time", property = "begin_time"),
            @Result(column = "end_time", property = "end_time")

    })
    @Select("select * from goods")
    List<Goods> SelectGoods();
}

service

@Service
public class GoodsService {
    @Resource
    SelectGoods selectGoods;

    public List<Goods> selectGoodsList(){
        List<Goods> goodsList = selectGoods.SelectGoods();
        return goodsList;
    }
}

controller

@RequestMapping("/index")
public String index(Model model){
    model.addAttribute("goodsList", goodsService.selectGoodsList());
    System.out.println(goodsService.selectGoodsList());
    return "index";
}

html

<div class="table-responsive">
    <table class="table table-styled mb-0">
        <thead>
        <tr>
            <th>id</th>
            <th>名称</th>
            <th>价格</th>
            <th>库存</th>
            <th>开始日期</th>
            <th>结束日期</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="goods:${goodsList}">
            <td th:text="${goods.getId()}">#JH2033</td>
            <td th:text="${goods.getGoods_name()}">#JH2033</td>
            <td th:text="${goods.getPrice()}">#JH2033</td>
            <td th:text="${goods.getStock()}">#JH2033</td>
            <td th:text="${#dates.format(goods.getBegin_time(), 'yyyy-MM-dd hh:mm:ss')}">#JH2033</td>
            <td th:text="${#dates.format(goods.getEnd_time(), 'yyyy-MM-dd hh:mm:ss')}">#JH2033</td>
        </tr>

        </tbody>
    </table>
</div>

结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ep95Kwlw-1664517455148)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\商品列表.png)]

商品详情

点击“详情“进行页面跳转,设置标签并传入商品id

<a th:href="@{/details(id=${goods.getId()})}">详情</a>

根据id进行商品查询

@Mapper
public interface SelectGoodsById {
    @Results({
            @Result(column = "id", property = "id"),
            @Result(column = "goods_name", property = "goods_name"),
            @Result(column = "price", property = "price"),
            @Result(column = "stock", property = "stock"),
            @Result(column = "begin_time", property = "begin_time"),
            @Result(column = "end_time", property = "end_time")

    })
    @Select("select * from goods where id=#{id}")
    Goods SelectGoodsById(int id);
}
public Goods selectGoodsDetails(int id){
    Goods goods = selectGoodsById.SelectGoodsById(id);
    return goods;
}

接受参数id并跳转到详情页面

@RequestMapping(value = "/details", method = RequestMethod.GET)
public String Details(Model model, @RequestParam("id") int goodsId)

详情页面

    <td th:text="${goods.getId()}">#JH2033</td>
    <td th:text="${goods.getGoods_name()}">#JH2033</td>
<td th:text="${goods.getPrice()}">#JH2033</td>
<td th:text="${goods.getStock()}">#JH2033</td>
<td th:text="${#dates.format(goods.getBegin_time(), 'yyyy-MM-dd hh:mm:ss')}">#JH2033</td>
<td th:text="${#dates.format(goods.getEnd_time(), 'yyyy-MM-dd hh:mm:ss')}">#JH2033</td>

详情页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-55y0W2QB-1664517455149)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\商品详情.png)]

秒杀倒计时

设置秒杀状态:>0距离秒杀的时间;=0秒杀进行中;-1秒杀已经结束

 @RequestMapping(value = "/details", method = RequestMethod.GET)
 public String Details(Model model, @RequestParam("id") int goodsId){
     Goods goods = goodsService.selectGoodsDetails(goodsId);
     //状态:1(秒杀未开始),0(进行中),-1(已结束)默认为0
     int secKillStatus = 0;
         Date nowDate = new Date();
         //秒杀未开始
         if (goods.getBegin_time().after(nowDate)) {
             secKillStatus=((int)(goods.getBegin_time().getTime()-nowDate.getTime())/1000);//秒杀为开始就将秒杀状态变为时间
         } else if (goods.getEnd_time().before(nowDate)) {
             secKillStatus=-1;
         }
     model.addAttribute("goods", goods);
         model.addAttribute("secKillStatus", secKillStatus);
     return "details";
 }

页面根据不同的状态进行显示

  <td>
      <label class="mb-0 badge badge-primary" title="" data-original-title="Pending" th:if="${secKillStatus gt 0}">倒计时:<span th:text="${secKillStatus}"></span></label>
      <label class="mb-0 badge badge-success" title="" data-original-title="Pending" th:if="${secKillStatus  eq 0}">秒杀进行中</label>
	<label class="mb-0 badge badge-primary" title="" data-original-title="Pending" th:if="${secKillStatus  eq -1}">已结束</label>
</td>

秒杀未开始时动态刷新倒计时,计时完毕后跳转到秒杀进行。利用js设置定时器动态刷新倒计时。

给页面中需要用的值设置id,js才能调用(大坑!,可以通过console.log(变量名)不断在浏览器控制台查看变量值调试)

<td>
    <!--必须加入id="",才能被js获取-->
    <input th:type="hidden" th:id="secKillStatus" th:value="${secKillStatus}">
    <label  th:if="${secKillStatus gt 0}">倒计时:<span id="countDown" th:text="${secKillStatus}"></span></label>
    <label  th:if="${secKillStatus  eq 0}">秒杀进行中</label>
    <label  th:if="${secKillStatus  eq -1}">已结束</label>
</td>

设置一个定时器,每隔1s执行一次

<!--定时器代码-->
<script >
    $(function (){
        countDown();
    });
    //cuntDown中继续调用cuntDown实现秒数递减
    function countDown(){
        var remainSeconds = $("#secKillStatus").val();
        var timeout;//定时器
        //秒杀未开始
        if(remainSeconds>0){
            timeout = setTimeout(function (){
                //将记录的状态值减去1
                $("#secKillStatus").val(remainSeconds-1);
                //将展示的值减1
                $("#countDown").text(remainSeconds-1);
                //继续调用
                countDown();
            },1000);
        }
        //如果倒计时定时器还存在,就清除定时器
        else if(remainSeconds==0){
            clearTimeout(timeout);
        }
    };
</script>

倒计时结束会停在0S,使用.html切换到“秒杀进行中”

<!--添加id用于js切换状态-->
<td th:id="secKillTip">
    <!--必须加入id="",才能被js获取-->
    <input th:type="hidden" th:id="secKillStatus" th:value="${secKillStatus}">
    <label  th:if="${secKillStatus gt 0}">倒计时:<span id="countDown" th:text="${secKillStatus}"></span></label>
    <label  th:if="${secKillStatus  eq 0}">秒杀进行中</label>
    <label  th:if="${secKillStatus  eq -1}">已结束</label>
</td>
<!--定时器代码-->
<script >
    $(function (){
        countDown();
    });
    //cuntDown中继续调用cuntDown实现秒数递减
    function countDown(){
        var remainSeconds = $("#secKillStatus").val();
        var timeout;//定时器
        //秒杀未开始
        if(remainSeconds>0){
            timeout = setTimeout(function (){
                //将记录的状态值减去1
                $("#secKillStatus").val(remainSeconds-1);
                //将展示的值减1
                $("#countDown").text(remainSeconds-1);
                //继续调用
                countDown();
            },1000);
        }
        //如果倒计时定时器还存在,就清除定时器
        else if(remainSeconds==0){
            clearTimeout(timeout);
            $("#secKillTip").html("秒杀进行中")
        }
        else {
            $("#secKillTip").html("已结束")
        }
    };
</script>

秒杀按钮

添加抢购按键,通过id可以操作按钮是否可用。

传回商品id进行后续操作

    <td>
    <form action="/doSekill" method="post">
        <!--抢购时需要传回商品ID-->
       <input type="hidden" name="goodsId" th:value="${goods.getId()}">
        <!--必须设置为btn才能禁用时变灰色 按钮为蓝色,充满一个区块,通过id控制是否可用-->
            <button class="btn btn-primary btn-block" type="submit" id="buyButton">立即抢购</button>
    </form>
</td>

在js代码中,调整按钮。在“秒杀进行中”状态时,秒杀按钮设置为可用,其余时候为不可用。

//秒杀按钮可用
$("#buyButton").attr("disabled", false);
//秒杀按钮置灰
$("#buyButton").attr("disabled", true);

页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZNTVmdP9-1664517455150)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\抢购页面.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dhEHQToY-1664517455151)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\不可抢购页面.png)]

秒杀功能

session添加用户

在秒杀界面需要用户id,在session中放入userData值。

    @Resource
    UserAuthService userAuthService;
@RequestMapping(value = "/details", method = RequestMethod.GET)
public String Details(Model model, @RequestParam("id") int goodsId, HttpSession httpSession) {
    //直接获取session中的UserData,有没有的逻辑交给service判断
    UserData userData = (UserData) userAuthService.findUser(httpSession);
    Goods goods = goodsService.selectGoodsDetails(goodsId);

通过service进行session中userData的判断与放置

    @Resource
    SelectUserByName selectUserByName;

    public UserData findUser(HttpSession httpSession) {
        //直接寻找
        UserData userData = (UserData) httpSession.getAttribute("UserData");
        //没有就去数据库查找用户,并放置session
        if (userData==null){
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            userData = selectUserByName.SelectUserByName(authentication.getName());
            httpSession.setAttribute("UserData", userData);
        }
        return userData;
    }

spring session实现

顺便实现sping sesssion,让关闭服务器后,同一session依然不需要登录。

redis安装:

redis GitHub Windows版本维护地址:https://github.com/tporadowski/redis/releases

GitHub 访问:使用https://ipaddress.com/website/github.global.ssl.fastly.net查询github ip,然后写入hosts文件并刷新dns。

第一次启动闪退,可以在安装位置 使用cmd依次执行:

redis-cli.exe
shutdown
exit
redis-server.exe redis.windows.conf

spring session实现:

导入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>
<!--spring session依赖-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

配置redis:

#redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=10000000

打开redis-cli.exe允许rdb存储 config set stop-writes-on-bgsave-error no

UserData存入redis需要进行序列化

@Data
public class UserData implements Serializable {
    int id;
    String name;
    String password;
    String roles;
}

session存入redis

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ooPUVcCa-1664517455152)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\session存入redis.png)]

设置完毕后,只要打开redis-server,就可以实现持久化session,关闭服务器并再次打开,依然不需要登录。

秒杀过程

需要满足条件:

  1. 有库存才能秒杀
  2. 每个用户限购一件

订单数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X2EvRUAU-1664517455153)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\订单数据库.png)]

详情页面传入商品id信息

<td>
    <form th:action="@{/doSeckill(id=${goods.getId()})}" method="post">
        <!--crsf的存在,必须在提交的form中写入以下信息-->
        <input th:name="${_csrf.parameterName}" th:value="${_csrf.token}" type="hidden">
        <!--抢购时需要传回商品ID-->
       <input type="hidden" name="goodsId" th:value="${goods.getId()}">
        <!--必须设置为btn才能禁用时变灰色 按钮为蓝色,充满一个区块,通过id控制是否可用-->
            <button class="btn btn-primary btn-block" type="submit" id="buyButton">立即抢购</button>
    </form>
</td>

购买逻辑实现

@RequestMapping(value = "/doSeckill",method = RequestMethod.POST)
   public String DoSeckill(Model model, HttpSession httpSession,@RequestParam("id") int goodsId){
       //取货物状态,判断库存
       Goods goods = goodsService.selectGoodsDetails(goodsId);
       if(goods.getStock()<1){
           model.addAttribute("FailMessage","库存不足");
           return "secKilFail";
       }
       //查看订单表,确认抢购一件
       UserData userData = userAuthService.findUser(httpSession);
       SecKillOrder secKillOrder = goodsService.selectOrder(userData.getId(), goods.getId());
       if(secKillOrder!=null){
           model.addAttribute("FailMessage","限购一件");
           return "secKillFail";
       }
       //减去库存,产生订单
       int nowStock = goods.getStock();
       goods.setStock(nowStock);
       goodsService.updateGoodsById(goods.getId(), goods.getStock());
       goodsService.insertSecKillOrder(userData.getId(), goods.getId());
       secKillOrder = goodsService.selectOrder(userData.getId(), goods.getId());
       model.addAttribute("SecKillOrder",secKillOrder);
    model.addAttribute("goods", goods);
    model.addAttribute("userData", userData);
       return "secKillOrder";
}

订单页面

<thead>
<tr>
    <th>id</th>
    <th>商品</th>
    <th>用户</th>
    <th>操作</th>
</tr>
</thead>
<tbody>

    <td th:text="${SecKillOrder.getId()}">null</td>
    <td th:text="${goods.getGoods_name()}">null</td>
    <td th:text="${userData.getName()}">null</td>
    <td>
    <button class="btn btn-primary btn-block" type="submit" id="buyButton">立即支付</button>
    </td>

</tbody>

订单展示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kogvQCcm-1664517455153)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\订单页面.png)]

jmeter使用

下载安装

进去jmeter官网下载binarie下的.zip文件,解压后在bin文件下打开jmter.bat

添加线程数,线程数*循环次数为总共的请求次数,设置在ramp-up内启动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q4pnlwA7-1664517455154)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter线程数.png)]

添加请求

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rj9fGd9E-1664517455155)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter添加请求.png)]

已经登录后,添加cookie,设置session进行登录才能访问商品列表。

多个用户进行请求就添加多个cookie。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pIyD3YPk-1664517455156)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter添加cookie.png)]

添加监听器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YHN2MCPZ-1664517455157)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter添加监听器.png)]

查看结果树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yp905Asm-1664517455157)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter查看结果树.png)]

查看聚合报告,吞吐量(qps)为每秒中成功处理的请求次数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pi0M0TFl-1664517455158)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter查看聚合报告.png)]

所有接口测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TVEmGxda-1664517455159)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter所有接口测试.png)]

页面优化

优化前 400线程 qps 192

页面缓存

不仅是跳转页面,而是返回一个页面并放到redis中缓存。

查询reidis-》返回/手动渲染-》存入redis-》返回模板

更改前的商品列表controller

//详情页面
    @RequestMapping("/index")
    public String index(Model model){
        List<Goods> goodsList = goodsService.selectGoodsList();
        model.addAttribute("goodsList", goodsList);
        return "index";
    }

加入页面缓存的商品列表controller

@Resource
RedisTemplate redisTemplate;
    @Resource
    ThymeleafViewResolver thymeleafViewResolver;

redis存入的缓存页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TdZjRbuq-1664517455159)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\redis存入的缓存页面.png)]

url缓存

进入不同的详情页面,点击后具有不同的id,按照以下方式获取

String html = (String) valueOperations.get("detailsTemplate:"+goodsId);//需要具有不同的id

改变后的详情页面controller

//秒杀页面
@RequestMapping(value = "/details", method = RequestMethod.GET, produces = "text/html;charset=utf-8")
@ResponseBody
public String Details(Model model, @RequestParam("id") int goodsId, HttpSession httpSession, HttpServletRequest request, HttpServletResponse response) {
       //进入不同的详情页面,缓存时需要存入id
    ValueOperations valueOperations = redisTemplate.opsForValue();//拿一个redis中存储键值对的工具
    String html = (String) valueOperations.get("detailsTemplate:"+goodsId);//需要具有不同的id
    if(!StringUtils.isEmpty(html)){
        return html;
    }

    //直接获取session中的UserData,有没有的逻辑交给service判断
    UserData userData = userAuthService.findUser(httpSession);
    Goods goods = goodsService.selectGoodsDetails(goodsId);
    //状态:1(秒杀未开始),0(进行中),-1(已结束)默认为0
    int secKillStatus = 0;
        Date nowDate = new Date();
        //秒杀未开始
        if (goods.getBegin_time().after(nowDate)) {
            secKillStatus=((int)(goods.getBegin_time().getTime()-nowDate.getTime())/1000);//秒杀为开始就将秒杀状态变为时间
        } else if (goods.getEnd_time().before(nowDate)) {
            secKillStatus=-1;
        }
        model.addAttribute("goods", goods);
        model.addAttribute("secKillStatus", secKillStatus);
    //页面手动渲染
    WebContext context = new WebContext(request, response, request.getServletContext(),
            request.getLocale(),model.asMap());//模板渲染需要传入Map形式数据
    html = thymeleafViewResolver.getTemplateEngine().process("details", context);//需要输入被渲染模板的名称
    if(!StringUtils.isEmpty(html)){
        //存入redis缓存
        valueOperations.set("detailsTemplate:"+goodsId, html, 60, TimeUnit.SECONDS);//设置失效时间为1分钟
        return html;
    }

    return null;
}

对象缓存

例如存入sping session到redis

需要注意的是更新密码后,需要清空redis中内容,保证数据一致性

加入页面缓存后

qps:192提高到qps:366

页面静态化

之前是前端需要渲染出整个带有服务端数据的页面,改为先渲染html,需要的数据再去查。

拆分出不需要变动的部分放到浏览器作缓存,只传输需要变动的部分(前端进行页面的跳转,ajax请求数据,后端只返回一个对象)

后端返回一个对象

值对象

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DetailsVo {
    UserData userData;
    Goods goods;
    int   secKillStatus;
}

商品详情页controller,返回一个对象

//静态化秒杀页面
@RequestMapping(value = "/details", method = RequestMethod.GET)
@ResponseBody
public DetailsVo Details(Model model, @RequestParam("id") int goodsId, HttpSession httpSession) {
    //直接获取session中的UserData,有没有的逻辑交给service判断
    UserData userData = userAuthService.findUser(httpSession);
    Goods goods = goodsService.selectGoodsDetails(goodsId);
    //状态:1(秒杀未开始),0(进行中),-1(已结束)默认为0
    int secKillStatus = 0;
    Date nowDate = new Date();
    //秒杀未开始
    if (goods.getBegin_time().after(nowDate)) {
        secKillStatus=((int)(goods.getBegin_time().getTime()-nowDate.getTime())/1000);//秒杀为开始就将秒杀状态变为时间
    } else if (goods.getEnd_time().before(nowDate)) {
        secKillStatus=-1;
    }
    DetailsVo detailsVo = new DetailsVo();
    detailsVo.setGoods(goods);
    detailsVo.setSecKillStatus(secKillStatus);
    detailsVo.setUserData(userData);
    return detailsVo;
}

页面处理

防止thymeleaf默认渲染,将详情页面拷贝到static下作为静态页面,并更改后缀为htm,并更改跳转到详情的href

<a th:href="'/static/details.htm?id='+${goods.getId()}">详情</a>

去掉所有src的th:,整个html页面现在是静态资源

使用th:传值的地方改为id,用ajax进行传值

//获取数据代码
function getDetails(){
//获取url中的id参数,确定请求的url
    var goodsId = g_getQueryString("id");
    //ajax向后端请求数据
    $.ajax({
        url:'/details',//这里为请求controller中对应方法,一定为单引号
        type:'GET',
        data:{
            "id":goodsId
        },//请求传入的参数,后端可以用@RequestParam获取
        // dataType:"json",
        success:function(data){//成功返回data后,就进行值提取
            render(data);
        },
        error:function (){
            window.alert("function请求出错");
    }
    })
}

render将获得的值赋予界面

function render(detail) {
    var goods = detail.goods;
    var secKillStatus = detail.secKillStatus;
    console.info("secKillStatus:"+secKillStatus);
    $("#id").text(goods.id);
    $("#goodsName").text(goods.goods_name);
    $("#goodsPrice").text(goods.price);
    $("#goodsStock").text(goods.stock);
    /*$("#goodsBeginTime").text(new Date(goods.begin_time).format(goods.begin_time, 'yyyy-MM-dd hh:mm:ss'));
    $("#goodsBeginTime").text(new Date(goods.end_time).format(goods.begin_time, 'yyyy-MM-dd hh:mm:ss'));*/
    $("#goodsBeginTime").text(goods.begin_time);
    $("#goodsEndTime").text(goods.end_time);
    $("#secKillStatus").val(secKillStatus);
    $("#goodsId").val(goods.id);
    countDown();

}

秒杀操作静态化

直接在页面进行跳转,更改秒杀按钮

<button class="btn btn-primary btn-block" type="button" id="buyButton" onclick="doSeckill()">立即抢购</button>

controller判断是否减去库存,返回跳转信息

    @RequestMapping(value = "/doSecKill",method = RequestMethod.GET)
    @ResponseBody
    public SecKillVo DoSeckill(Model model, HttpSession httpSession, @RequestParam("id") int goodsId){
        //取货物状态,判断库存
        Goods goods = goodsService.selectGoodsDetails(goodsId);
        SecKillVo secKillVo =new SecKillVo();
        if(goods.getStock()<1){
            model.addAttribute("FailMessage","库存不足");
            secKillVo.setMessage("Fail");
            return secKillVo;
        }
        //查看订单表,确认抢购一件
        UserData userData = userAuthService.findUser(httpSession);
        SecKillOrder secKillOrder = goodsService.selectOrder(userData.getId(), goods.getId());
        if(secKillOrder!=null){
            model.addAttribute("FailMessage","限购一件");
            secKillVo.setMessage("Fail");
            return secKillVo;
        }
        //减去库存,产生订单
        int nowStock = goods.getStock();
        goods.setStock(nowStock-1);
        goodsService.updateGoodsById(goods.getId(), goods.getStock());
        goodsService.insertSecKillOrder(userData.getId(), goods.getId());
        secKillVo.setMessage("Success");
        return secKillVo;
    }

js书写秒杀方法,向后台请求数据,并使用window.location.href跳转至订单页面

    function doSecKill(){
        var goodsId = $("#goodsId").val();
        $.ajax(
            {
             url:'/doSecKill' ,
             type: 'GET',
             data:{
                 "id":goodsId//键要有双引号
             },
             success:function (data){
                 if(data.message=="Success"){
                     window.location.href="/static/secKillOrder.htm?id="+goodsId;
                 }
                 else {
                     window.location.href="/static/secKillFail.htm";
                 }
             },
             error:function (){
                 window.alert("function错误")
             }
            }
        )
    }

订单页面静态化

controller返回订单结果

    @RequestMapping(value = "/OrderDetail",method = RequestMethod.GET)
    @ResponseBody
    public OrderDetailVo OrderDetail(Model model, HttpSession httpSession, @RequestParam("id") int goodsId){
        Goods goods = goodsService.selectGoodsDetails(goodsId);
        UserData userData = userAuthService.findUser(httpSession);
        SecKillOrder secKillOrder = goodsService.selectOrder(userData.getId(), goods.getId());
        OrderDetailVo orderDetail = new OrderDetailVo();
        orderDetail.setSecKillOrder(secKillOrder);
        orderDetail.setGoods(goods);
        orderDetail.setUserData(userData);
        return orderDetail;
    }

订单页面ajax请求

    $(function (){
        OrderDetail();
    });
    function OrderDetail(){
        var goodsId = g_getQueryString("id");
      $.ajax({
          url:'/OrderDetail',
          type:'GET',
          data:{
            "id":goodsId
          },
          success:function (data){
              var secKillOrder = data.secKillOrder;
              var goods = data.goods;
              var userData = data.userData;
              $("#orderId").text(secKillOrder.id);
              $("#goodsName").text(goods.goods_name);
              $("#userName").text(userData.name);
          },
          error:function (){
              window.alert("function错误")
          }
      })
    }

    //获取Url中参数
    function g_getQueryString(name){
        var reg = new RegExp("(^|&)"+name+"=([^&]*)(&|$)");//正则表达式
        var r = window.location.search.substr(1).match(reg);//从?开始的url部分
        if(r!=null) return unescape(r[2]);
        return null;
    }

超卖

库存一直减为负数:减去时判断库存是否大于0

两个请求同时发出,一个用户可能抢到两件商品:同一用户超卖,可以将uid和goodid组合添加为唯一索引

qps->290

服务优化

解决高并发

预减库存

减少数据库访问

在最开始将数据库中库存,加载到redis,每次从redis中减去库存,成功就给到消息队列

redis预减库存,不足直接返回。实现InitializingBean,在controller初始化时将库存加载到redis。

@Controller
public class MainController implements InitializingBean
 @Override
    public void afterPropertiesSet() throws Exception {
        List<Goods> goodsList = goodsService.selectGoodsList();
        if(goodsList==null){return;}
        else {
            goodsList.forEach(goods -> {
                redisTemplate.opsForValue().set("secKillGoods:"+goods.getId(), goods.getStock());
            });
        }
    }

秒杀时的预减去库存

int stock = (int) redisTemplate.opsForValue().get("secKillGoods:"+goods.getId());
if(stock<=0){
    model.addAttribute("FailMessage","库存不足");
    secKillVo.setMessage("Fail");
    return secKillVo;
}

内存标记

减少reids访问

定义存储标记的map

Map<Integer, Boolean> EmptyStock =new HashMap<>();

初始化时,将标记设置为false

@Override
public void afterPropertiesSet() throws Exception {
    List<Goods> goodsList = goodsService.selectGoodsList();
    if(goodsList==null){return;}
    else {
        goodsList.forEach(goods -> {
            redisTemplate.opsForValue().set("secKillGoods:"+goods.getId(), goods.getStock());
            EmptyStock.put(goods.getId(), false);
        });
    }
}

当库存为0,设置内存标记为true

if(stock<=0){
    model.addAttribute("FailMessage","库存不足");
    secKillVo.setMessage("Fail");
    EmptyStock.put(goodsId,true);
    return secKillVo;
}

接下来再进行尝试减去库存之前,如果内存标记为true,直接返回失败

if (EmptyStock.get(goodsId)){
    secKillVo.setMessage("Fail");
    return secKillVo; 
}
int stock = (int) redisTemplate.opsForValue().get("secKillGoods:"+goods.getId());

队列消息缓冲

ubuntu 22.04中安装

sudo apt install erlang
sudo apt install rabbitmq-server

查询安装状态、开启管理面板

sudo rabbitmqctl status
sudo rabbitmq-plugins enable rabbitmq_management

linux中使用ifconfig -a查看本机网络ip 本地网络ip:15672 可访问控制面板

guest用户只能本地登录 因此创建新的用户

sudo rabbitmqctl add_user 用户名 密码
sudo rabbitmqctl set_user_tags 用户名 administrator

交换机(使用@Bean方法中return new不同类型的交换机):fanout(广播)、direct(使用routing key交换机和队列)、topic(仍然使用routing 不过是通配符 *匹配一个词语 #匹配一个或多个词语)

SpringBoot集成RabbitMQ:
1.添加RabbitMQ依赖(amqp)

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

2.配置RabbitMQ(主机(host)、用户名(username),密码(password))192.168.209.129

#rabbimq
spring.rabbitmq.host=192.168.209.129
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin

3.config中RabbitMQ中配置类,这里使用topic模式(方法上@Bean, return new Queue(“队列名称”, 是否持久化), return new TopicExchange(“交换机名称”, 是否持久化), return new Bindingbuilder.bind().with(“匹配的队列名称”))

@Configuration
public class RabbitMQConfiguration {
    //队列
    @Bean
    public Queue queue(){
        return new Queue("seckillQueue");
    }
    //交换机
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange("seckillExchange");
    }
    //绑定
    @Bean
    public Binding binding(){
        return BindingBuilder.bind(queue()).to(topicExchange()).with("secKill.#");
    }
}

4.消息定义(实体类(秒杀信息),返回用户和货物)

public class SeckillMessage {
    Goods goods;
    UserData userData;
}

5.消息发送者(rabbimq包下,类上@Service ,注入RabbimqTemplate,定义send方法,使用模板中convertAndSend.(“交换机名称”,“路由名称”,消息)

导入依赖

<!--json与对象互转-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.54</version>
</dependency>
@Service
public class RabbiMQSender {
    @Resource
    RabbitTemplate rabbitTemplate;

    public void send(SeckillMessage seckillMessage){
        rabbitTemplate.convertAndSend("seckillExchange", "seckillMessage",JSON.toJSONString(seckillMessage));
    }
}

6.消息接收者(rabbimq包下,类上@Service , @RabbitMQ(“队列名称”), 监听队列),获取信息后,进行下单操作

@Service
public class RabbitMQRecive {
    @Resource
    GoodsService goodsService;
    
    @RabbitListener(queues = "seckillQueue")
    public SecKillVo receive(String message){
        SeckillMessage seckillMessage = JSON.parseObject(message, SeckillMessage.class);
        //进行秒杀
        Goods goods = seckillMessage.getGoods();
        UserData userData = seckillMessage.getUserData();
        int nowStock = goods.getStock();
        goods.setStock(nowStock-1);
        goodsService.updateGoodsById(goods.getId(), goods.getStock());
        goodsService.insertSecKillOrder(userData.getId(), goods.getId());
        
        SecKillVo secKillVo = new SecKillVo();
        secKillVo.setMessage("Wait");
        return secKillVo;
    }
}

7.controller调用发送消息并返回等待

SeckillMessage seckillMessage = new SeckillMessage(goods, userData);
rabbiMQSender.send(seckillMessage);
secKillVo.setMessage("Wait");
return secKillVo;

8.客户端轮询(去在客户端中查询订单表,如果获取就返回1表示有订单)

function doSecKill(){
    var goodsId = $("#goodsId").val();
    $.ajax(
        {
         url:'/doSecKill' ,
         type: 'GET',
         data:{
             "id":goodsId//键要有双引号
         },
         success:function (data){
             if(data.message=="Wait"){
                 getResult(id);
             }
             else {
                 window.location.href="/static/secKillFail.htm";
             }
         },
         error:function (){
             window.alert("function错误")
         }
        }
    )
}

function getResult(goodsId){
   $.ajax({
       ulr:'/getResult',
       type:'GET',
       data:{
           "id":goodsId//键要有双引号
       },
       success:function (data){
           if(data.message=="Wait"){
           setTimeout(function (){
               getResult(goodsId);
           },50)}
           else {
               layer.confirm("秒杀成功,查看订单?", {btn:["确认","取消"]},
               function (){
                   window.location.href="/static/secKillOrder.htm"+goodsId;
               })
           }
       }
   }) 
}

9.查询查询订单的controller

@RequestMapping(value = "/getResult",method = RequestMethod.GET)
public SecKillVo getResult(HttpSession httpSession, @RequestParam("id") int goodsId){
    UserData userData = userAuthService.findUser(httpSession);
    SecKillOrder secKillOrder = goodsService.selectOrder(userData.getId(), goodsId);
    
    SecKillVo secKillVo = new SecKillVo();
    secKillVo.setMessage("Success");
    return secKillVo;
}

配置类中定义消息队列,sercie中定义与队列绑定的生产者消费者,contrller中发送一个消息

定义队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lahYRk2C-1664517455161)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\消息队列配置.png)]

controller传入用户和商品id,使用消息队列处理,当给到消息队列后,返回0表示排队中。

最终的下单结果,在客户端中的js使用轮询查看是否真正下单成功。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XOY5S8iM-1664517455162)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\controller使用消息队列.png)]

发送消息到队列,请求生成订单,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9J7t8fR1-1664517455162)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\消息队列发送消息.png)]

从队列中取得消息,尝试生成订单

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-59J5Fi8r-1664517455163)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\消息队列消费消息.png)]

客户端轮询结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PFNsdmDU-1664517455164)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\客户端轮询结果.png)]

reids分布式锁

原本的redis递增递减带有原子性,可以优使用redis分布式锁优化。

安全优化

秒杀接口隐藏

每个用户拥有不同的秒杀接口地址,防止脚本不断的刷新。现在脚本无法得到真正的秒杀接口,因为秒杀接口需要特定生成。防止攻击地址,现在需要点击秒杀按钮才能得到真正的地址,并且每个用户不一样。这样同一个用户多次点击攻击后,可以使用接口限流隐藏掉。

每次点击秒杀,不是直接秒杀,而是获取接口地址,同时中间加验证码,防止脚本。

点击秒杀按钮后,首先均访问/path的Controller,在该Controller中生成唯一的接口地址,存储在redis中key为skillpath+uerid+goodsid,值为生成的秒杀地址,并在前端页面接受。前端接受到生成的path之后,进行真正的秒杀。

接口限流

每个用户请求几次后,禁止请求。

用户判断:ConcurrentHashmap(用户,次数)

goodsId;
})
}
}
})
}


9.查询查询订单的controller

```java
@RequestMapping(value = "/getResult",method = RequestMethod.GET)
public SecKillVo getResult(HttpSession httpSession, @RequestParam("id") int goodsId){
    UserData userData = userAuthService.findUser(httpSession);
    SecKillOrder secKillOrder = goodsService.selectOrder(userData.getId(), goodsId);
    
    SecKillVo secKillVo = new SecKillVo();
    secKillVo.setMessage("Success");
    return secKillVo;
}

配置类中定义消息队列,sercie中定义与队列绑定的生产者消费者,contrller中发送一个消息

定义队列

[外链图片转存中…(img-lahYRk2C-1664517455161)]

controller传入用户和商品id,使用消息队列处理,当给到消息队列后,返回0表示排队中。

最终的下单结果,在客户端中的js使用轮询查看是否真正下单成功。

[外链图片转存中…(img-XOY5S8iM-1664517455162)]

发送消息到队列,请求生成订单,

[外链图片转存中…(img-9J7t8fR1-1664517455162)]

从队列中取得消息,尝试生成订单

[外链图片转存中…(img-59J5Fi8r-1664517455163)]

客户端轮询结果

[外链图片转存中…(img-PFNsdmDU-1664517455164)]

reids分布式锁

原本的redis递增递减带有原子性,可以优使用redis分布式锁优化。

安全优化

秒杀接口隐藏

每个用户拥有不同的秒杀接口地址,防止脚本不断的刷新。现在脚本无法得到真正的秒杀接口,因为秒杀接口需要特定生成。防止攻击地址,现在需要点击秒杀按钮才能得到真正的地址,并且每个用户不一样。这样同一个用户多次点击攻击后,可以使用接口限流隐藏掉。

每次点击秒杀,不是直接秒杀,而是获取接口地址,同时中间加验证码,防止脚本。

点击秒杀按钮后,首先均访问/path的Controller,在该Controller中生成唯一的接口地址,存储在redis中key为skillpath+uerid+goodsid,值为生成的秒杀地址,并在前端页面接受。前端接受到生成的path之后,进行真正的秒杀。

接口限流

每个用户请求几次后,禁止请求。

用户判断:ConcurrentHashmap(用户,次数)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值