前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >玩转 Spring Boot 应用篇(搭建菜菜的店铺)

玩转 Spring Boot 应用篇(搭建菜菜的店铺)

作者头像
一猿小讲
发布2022-05-31 09:42:34
4670
发布2022-05-31 09:42:34
举报
文章被收录于专栏:一猿小讲一猿小讲

0.

0.0. 历史文章整理

玩转 Spring Boot 入门篇

玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)

玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)

玩转 Spring Boot 集成篇(Redis)

玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)

玩转 Spring Boot 集成篇(RabbitMQ)

玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)

玩转 Spring Boot 集成篇(任务动态管理代码篇)

玩转 Spring Boot 集成篇(定时任务框架Quartz)

玩转 Spring Boot 原理篇(源码环境搭建)

玩转 Spring Boot 原理篇(核心注解知多少)

玩转 Spring Boot 原理篇(自动装配前凑之自定义Starter)

玩转 Spring Boot 原理篇(自动装配源码剖析)

玩转 Spring Boot 原理篇(启动机制源码剖析)

玩转 Spring Boot 原理篇(内嵌Tomcat实现原理&优雅停机源码剖析)

0.1. 背景

菜菜同学前几天与好友一起去环球影城溜达了一天,亲自拍了一系列富有感情的珍藏版照片。为了能更好的输出高质量的技术文章,菜菜想要把照片挂到网站上卖掉来换点银子,然后把写作装备更新一波。

不过,菜菜同学需要快速搭建一个商品售卖网站(菜菜的店铺),以便能够把照片尽快卖掉换点银子 ... ...

未曾想建设店铺途中,遇到了超卖、高并发以及瞬间过高的请求导致访问高峰等一系列的问题,不过终究都被菜菜给化解啦,咱们后面慢慢去谈。

0.2. 技术选型

  • 技术选型:Spring Boot 2.6.3 + MyBatis 3.5.9 + Thymeleaf 3.0.14 + Bootstrap 5.1.3 +Redis 6.2.6+ RabbitMQ 3.9.13 + MySQL
  • 依赖环境:JDK 1.8 + Maven 3.6.3
  • 开发工具:IntelliJ IDEA

0.3. 项目演示

  • 店铺首页
  • 点击“买它”按钮,成功购买页面效果。
  • 点击“买它”按钮,购买失败页面效果。

1. 从 0 开始,动手操练起来。

1.1. 设计数据库表

代码语言:javascript
复制
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
--  Table structure for `t_goods`
-- ----------------------------
DROP TABLE IF EXISTS `t_goods`;
CREATE TABLE `t_goods` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品编号',
  `name` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '商品名称',
  `image` varchar(255) CHARACTER SET utf8 NOT NULL COMMENT '商品图片',
  `stock` int NOT NULL DEFAULT '0' COMMENT '商品数量',
  `price` decimal(10,2) DEFAULT NULL COMMENT '价格',
  `start_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '秒杀开始时间',
  `end_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '秒杀结束时间',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

-- ----------------------------
--  Records of `t_goods`
-- ----------------------------
BEGIN;
INSERT INTO `t_goods` VALUES ('1', '功夫熊猫', '/gfxm.jpg', '10', '666.66', '2022-04-06 17:41:24', '2022-04-06 17:41:24', '2022-04-06 17:41:24', '2022-04-06 17:41:24'), ('2', '威震天', '/wzt.jpg', '8', '888.88', '2022-04-06 17:41:28', '2022-04-06 17:41:28', '2022-04-06 17:41:28', '2022-04-06 17:41:28'), ('3', '小黄人乐翻天', '/xhr.jpg', '6', '777.77', '2022-04-06 17:41:32', '2022-04-06 17:41:32', '2022-04-06 17:41:32', '2022-04-06 17:41:32');
COMMIT;

-- ----------------------------
--  Table structure for `t_user_goods`
-- ----------------------------
DROP TABLE IF EXISTS `t_user_goods`;
CREATE TABLE `t_user_goods` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '唯一主键',
  `user_id` bigint DEFAULT NULL COMMENT '用户id',
  `goods_id` bigint DEFAULT NULL COMMENT '商品id',
  `quantity` int DEFAULT '0' COMMENT '数量',
  `state` tinyint DEFAULT NULL COMMENT '状态,-1:无效;0:成功;1:已付款',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

SET FOREIGN_KEY_CHECKS = 1;

数据库表设计很简单,一张商品信息表,一张用户购买记录表。

1.2. 创建项目骨架

采用 IDEA 创建 Spring Boot Web 项目(引入 web 依赖包)

代码语言:javascript
复制
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

项目名为 caicaishop。

项目创建后,直接运行 main 函数,看看环境是否 OK,正常启动会输出。

1.3. 集成 MyBatis

  • pom.xml 加入依赖
代码语言:javascript
复制
<!-- 引入 MyBatis 依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybaits-spring-boot-starter</artifactId>
    <version>2.2.1</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
  • 添加数据库连接& MyBatis 配置

在 application.properties 文件中添加配置信息。

代码语言:javascript
复制
# MySQL 链接信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/caicaishop?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

## MyBatis 的配置
# Mapper资源文件存放的路径
mybatis.mapper-locations=classpath*:mapper/*Mapper.xml
# Dao 接口文件存放的目录
mybatis.type-aliases-package=org.growup.caicaishop.dao
# 开启 debug,输出 SQL
logging.level.org.growup.caicaishop.dao=debug

1.4. 创建实体类

首先创建 entity 目录,用于存放实体类源代码文件。

  • 创建 Goods 实体类
代码语言:javascript
复制
/**
 * 商品信息类
 */
public class Goods implements Serializable {

    private Integer id;

    private String name;

    private String image;

    private Integer stock;

    private BigDecimal price;

    private Timestamp startTime;

    private Timestamp endTime;

    private Timestamp createTime;

    private Timestamp updateTime;

    // 提供 setter / getter 方法
}
  • 创建 UserGoods 实体类
代码语言:javascript
复制
/**
 * 用户商品购买记录
 */
public class UserGoods implements Serializable {

    private Integer id;

    private Integer userId;

    private Integer goodsId;

    private Integer quantity;

    private Integer state;

    private Timestamp createTime;

    private Timestamp updateTime;

    // 提供 setter / getter 方法
    // 提供 toString 方法
}

1.5. 创建 Dao

  • 创建商品Dao(GoodsDao)

提供查询单个商品、查询所有商品以及减库存的方法定义。

代码语言:javascript
复制
package org.growup.caicaishop.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.growup.caicaishop.entity.Goods;

import java.util.List;

@Mapper
public interface GoodsDao {

    /**
     * 查询商品
     */
    public Goods getGoodsById(@Param("id")Integer id);

    /**
     * 查询所有商品
     */
    public List<Goods> selectAll();

    /**
     * 减库存
     */
    public int reduceStock(@Param("id")Integer id, @Param("quantity") int quantity);
}
  • 创建用户商品购买记录Dao(UserGoodsDao)

提供保存用户购买记录的方法定义。

代码语言:javascript
复制
package org.growup.caicaishop.dao;

import org.apache.ibatis.annotations.Mapper;
import org.growup.caicaishop.entity.UserGoods;

@Mapper
public interface UserGoodsDao {
    /**
     * 插入用户购买记录
     */
    public int insert(UserGoods userGoods);
}

1.6. 创建 Mapper 文件

在 resources 目录下创建 mapper 文件夹,用于存放 mapper 文件。

  • GoodsMapper.xml
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.growup.caicaishop.dao.GoodsDao">

    <resultMap id="BaseResultMap" type="org.growup.caicaishop.entity.Goods">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="image" property="image" jdbcType="VARCHAR"/>
        <result column="stock" property="stock" jdbcType="INTEGER"/>
        <result column="price" property="price" jdbcType="DECIMAL"/>
        <result column="start_time" property="startTime" jdbcType="TIMESTAMP"/>
        <result column="end_time" property="endTime" jdbcType="TIMESTAMP"/>
        <result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
        <result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
    </resultMap>

    <select id="getGoodsById" resultMap="BaseResultMap">
        select id, name, image, stock, price,start_time,end_time,create_time, update_time from t_goods where id = #{id,jdbcType=INTEGER}
    </select>

    <select id="selectAll" resultMap="BaseResultMap">
        select id, name, image, stock, price,start_time,end_time,create_time, update_time from t_goods
    </select>

    <update id="reduceStock">
        update t_goods set stock = stock - #{quantity} where id = #{id,jdbcType=INTEGER}
    </update>
</mapper>
  • UserGoodsMapper.xml
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.growup.caicaishop.dao.UserGoodsDao">
    <insert id="insert" parameterType="org.growup.caicaishop.entity.UserGoods">
        insert into t_user_goods (user_id, goods_id, quantity, state, create_time)
        values (#{userId,jdbcType=INTEGER}, #{goodsId,jdbcType=INTEGER}, #{quantity,jdbcType=INTEGER},
                #{state,jdbcType=INTEGER}, #{createTime,jdbcType=TIMESTAMP})
    </insert>
</mapper>

1.7. 创建 Service 接口

  • 商品Service(GoodsService)
代码语言:javascript
复制
package org.growup.caicaishop.service;

import org.growup.caicaishop.entity.Goods;

import java.util.List;

public interface GoodsService {

    /**获取商品列表*/
    public List<Goods> findAllGoods();

    /**减库存*/
    public int reduceStock(Integer goodsId,int quantity);
}
  • 用户商品购买记录Service(UserGoodsService)
代码语言:javascript
复制
package org.growup.caicaishop.service;

import org.growup.caicaishop.entity.UserGoods;

public interface UserGoodsService {
    /**
     * 保存用户购买记录
     */
    public int save(UserGoods userGoods);
}
  • 处理购买业务的Service(PurchaseService)
代码语言:javascript
复制
package org.growup.caicaishop.service;

public interface PurchaseService {
    /**
     * 处理购买业务
     */
    public boolean purchase(Integer userId, Integer goodsId, int quantity);
}

1.8. 创建 Service 接口的实现类

  • 商品Service实现类(GoodsServiceImpl)
代码语言:javascript
复制
package org.growup.caicaishop.service.impl;

import org.growup.caicaishop.dao.GoodsDao;
import org.growup.caicaishop.entity.Goods;
import org.growup.caicaishop.service.GoodsService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class GoodsServiceImpl implements GoodsService {

    @Resource
    private GoodsDao goodsDao;

    @Override
    public List<Goods> findAllGoods() {
        return goodsDao.selectAll();
    }

    @Override
    public int reduceStock(Integer goodsId,int quantity) {
        return goodsDao.reduceStock(goodsId,quantity);
    }
}
  • 用户购买商品记录Service实现类(UserGoodsServiceImpl)
代码语言:javascript
复制
package org.growup.caicaishop.service.impl;

import org.growup.caicaishop.dao.UserGoodsDao;
import org.growup.caicaishop.entity.UserGoods;
import org.growup.caicaishop.service.UserGoodsService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class UserGoodsServiceImpl implements UserGoodsService {

    @Resource
    private UserGoodsDao userGoodsDao;

    @Override
    public int save(UserGoods userGoods) {
        return userGoodsDao.insert(userGoods);
    }
}
  • 购买逻辑处理Service实现类(PurchaseServiceImpl)
代码语言:javascript
复制
package org.growup.caicaishop.service.impl;

import org.growup.caicaishop.dao.GoodsDao;
import org.growup.caicaishop.dao.UserGoodsDao;
import org.growup.caicaishop.entity.Goods;
import org.growup.caicaishop.entity.UserGoods;
import org.growup.caicaishop.service.PurchaseService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.sql.Timestamp;
import java.util.logging.Logger;

@Service
public class PurchaseServiceImpl implements PurchaseService {

    private final Logger logger = Logger.getLogger("PurchaseServiceImpl");

    @Resource
    private GoodsDao goodsDao;

    @Resource
    private UserGoodsDao userGoodsDao;

    @Override
    @Transactional
    public boolean purchase(Integer userId, Integer goodsId, int quantity) {
        Goods goodsInfo = goodsDao.getGoodsById(goodsId);
        if (goodsInfo.getStock() < quantity) {
            // 库存不足
            logger.info("库存不足: " + goodsInfo.getStock());
            return false;
        }

        int res = goodsDao.reduceStock(goodsId, quantity);
        logger.info("扣减库存结果:" + res);
        //插入购买记录
        UserGoods userGoods = new UserGoods();
        userGoods.setUserId(userId);
        userGoods.setGoodsId(goodsId);
        userGoods.setQuantity(quantity);
        userGoods.setState(1);
        userGoods.setCreateTime(new Timestamp(System.currentTimeMillis()));
        int saveRes = userGoodsDao.insert(userGoods);
        logger.info("插入购买记录:" + saveRes);
        return saveRes == 1;
    }
}

至此,实体类、Dao、Service 等数据库相关封装操作基本完事儿,跑单元测试验证一下。

1.9. 单元测试验证

代码语言:javascript
复制
package org.growup.caicaishop;

import org.growup.caicaishop.entity.Goods;
import org.growup.caicaishop.entity.UserGoods;
import org.growup.caicaishop.service.GoodsService;
import org.growup.caicaishop.service.PurchaseService;
import org.growup.caicaishop.service.UserGoodsService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.sql.Timestamp;
import java.util.List;
import java.util.logging.Logger;

@SpringBootTest
class CaicaishopApplicationTests {

    private final Logger logger = Logger.getLogger("CaicaishopApplicationTests");

    @Autowired
    private GoodsService goodsService;

    @Autowired
    private UserGoodsService userGoodsService;

    @Autowired
    private PurchaseService purchaseService;

    @Test
    public void testFindAllGoods() {
        List<Goods> goodsList = goodsService.findAllGoods();
        logger.info("商品信息:" + goodsList);
    }

    @Test
    public void testSaveUserGoods() {
        UserGoods userGoods = new UserGoods();
        userGoods.setUserId(10086);
        userGoods.setGoodsId(1);
        userGoods.setQuantity(1);
        userGoods.setState(1);
        userGoods.setCreateTime(new Timestamp(System.currentTimeMillis()));
        logger.info("保存用户购买商品记录结果:" + userGoodsService.save(userGoods));
    }

    @Test
    public void testPurchase() {
        boolean purchaseRes = purchaseService.purchase(1,1,1);
        logger.info("商品购买结果:" + purchaseRes);
    }
}
  • 验证 GoodService 中获取商品列表功能。
  • 验证 UserGoodsService 中保存用户购买商品记录功能。
  • 验证 PurchaseService 中商品购买功能。

至此,数据库层面的 CRUD 封装完成,验证通过。

2. 控制层实现

  • 首页控制器(IndexController)
代码语言:javascript
复制
package org.growup.caicaishop.controller;

import org.growup.caicaishop.service.GoodsService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Resource;

@Controller
public class IndexController {

    @Resource
    private GoodsService goodsService;

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

主要是查询商品列表,然后返回给 index 视图。

  • 商品购买控制器(PurchaseController)
代码语言:javascript
复制
package org.growup.caicaishop.controller;

import org.growup.caicaishop.service.PurchaseService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;

@Controller
public class PurchaseController {

    @Resource
    private PurchaseService purchaseService;

    /**
     * 购买商品,直接返回视图
     */
    @RequestMapping("/purchase")
    public String purchase(Model model, @RequestParam("goodsId") Integer goodsId,
                           @RequestParam("userId") Integer userId) {
        boolean isOk = purchaseService.purchase(userId, goodsId, 1);
        model.addAttribute("msg", isOk ? "恭喜您,购买成功^_^" : "很遗憾,别灰心,继续买...");
        return "notice";
    }

    /**
     * 购买商品 API,供前端模拟并发使用
     */
    @CrossOrigin
    @ResponseBody
    @RequestMapping("/api/purchase")
    public String purchaseAPI(Integer goodsId, Integer userId) {
        boolean isOk = purchaseService.purchase(userId, goodsId, 1);
        return isOk ? "恭喜您,购买成功^_^" : "很遗憾,别灰心,继续买...";
    }
}

提供了两种方式:一种是直接返回视图,一种是供模拟并发调用的 API 。

3. 集成 Thymeleaf & 展示层实现

3.1 集成 Thymeleaf

Thymeleaf 是一个 Java XML / XHTML / HTML5 模板引擎 ,可以在 Web(基于servlet )和非 Web 环境中工作。它更适合在基于 MVC 的 Web 应用程序的视图层提供 XHTML / HTML5,但它甚至可以在脱机环境中处理任何 XML 文件。它提供完整的 Spring Framework。 在 Web 应用程序中,Thymeleaf 旨在成为 JavaServer Pages(JSP)的完全替代品,并实现自然模板的概念:模板文件可以直接在浏览器中打开,并且仍然可以正确显示为网页。

  • 引入 thymeleaf 依赖
代码语言:javascript
复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • thymeleaf 相关配置

在 application.properties 文件中加入如下配置。

代码语言:javascript
复制
### thymeleaf 配置
# 模板的模式,支持如 HTML、XML、TEXT、JAVASCRIPT 等
spring.thymeleaf.mode=HTML
# 编码,可不用配置
spring.thymeleaf.encoding=UTF-8
# 内容类别,可不用配置
spring.thymeleaf.servlet.content-type=text/html
# 开发环境下配置为 false,避免修改模板还需要重启服务器
spring.thymeleaf.cache=false
# 配置模板路径,默认就是 templates,可不用配置
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

3.2 展示层实现

  • 店铺首页(index.html)
代码语言:javascript
复制
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>goods list</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="/css/pricing.css" rel="stylesheet" type="text/css">
</head>

<body>
<div class="container py-3">
    <header>
        <div class="d-flex flex-column flex-md-row align-items-center pb-3 mb-4 border-bottom">
            <a href="/" class="d-flex align-items-center text-dark text-decoration-none">
                <svg xmlns="http://www.w3.org/2000/svg" width="40" height="32" class="me-2" viewBox="0 0 118 94"
                     role="img"><title>CaiCaiShop</title>
                    <path fill-rule="evenodd" clip-rule="evenodd"
                          d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z"
                          fill="currentColor"/>
                </svg>
                <span class="fs-4 text-success">菜菜的店铺</span>
            </a>
        </div>

        <div class="pricing-header p-3 pb-md-4 mx-auto text-center">
            <h1 class="display-4 fw-normal text-success">生活就是买买买</h1>
            <p/>
            <ul class="list-group list-group-horizontal">
                <li class="list-group-item list-group-item-danger">有钱不花,掉了白搭</li>
                <li class="list-group-item list-group-item-warning">钱是王八蛋,没了咱再赚</li>
                <li class="list-group-item list-group-item-info">每天不是买买买,就是去买买买的路上</li>
            </ul>
        </div>
    </header>

    <main>
        <div class="row row-cols-1 row-cols-md-3 mb-3 text-center">
            <div class="col" th:each="goods:${goodsList}">
                <div class="card mb-4 rounded-3 shadow-sm border-success">
                    <div class="card-header py-3 text-white bg-success border-success">
                        <h4 class="my-0 fw-normal" th:text="${goods.name}"></h4>
                    </div>
                    <div class="card-body">
                        <h1 class="card-title pricing-card-title">
                            $ <small class="text-success fw-light" th:text="${goods.price}"/>
                        </h1>
                        <ul class="list-unstyled mt-3 mb-4">
                            <li>库存剩余:<span class="badge bg-danger badge-pill" th:text="${goods.stock}"/></li>
                            <li><img width="110px" height="120px" th:src="@{'/images'+${goods.image}}"/></li>
                            <li>开始时间:<small class="text-success fw-light" th:text="${goods.startTime}"/></li>
                            <li>结束时间:<small class="text-success fw-light" th:text="${goods.endTime}"/></li>
                        </ul>
                        <a type="button" class="w-100 btn btn-lg btn-outline-success"
                           th:href="@{'/purchase?userId=1&goodsId='+${goods.id}}">买它</a>
                    </div>
                </div>
            </div>
        </div>
    </main>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"/>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"/>
</html>
  • 商品购买通知页面(notice.html)
代码语言:javascript
复制
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>purchase result</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="/css/pricing.css" rel="stylesheet" type="text/css">
</head>

<body>
<div class="container py-3">
    <header>
        <div class="d-flex flex-column flex-md-row align-items-center pb-3 mb-4 border-bottom">
            <a href="/" class="d-flex align-items-center text-dark text-decoration-none">
                <svg xmlns="http://www.w3.org/2000/svg" width="40" height="32" class="me-2" viewBox="0 0 118 94"
                     role="img"><title>CaiCaiShop</title>
                    <path fill-rule="evenodd" clip-rule="evenodd"
                          d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z"
                          fill="currentColor"/>
                </svg>
                <span class="fs-4">菜菜的店铺</span>
            </a>
        </div>

        <div class="pricing-header p-3 pb-md-4 mx-auto text-center">
            <h1 class="display-4 fw-normal text-success">生活就是买买买</h1>
            <p/>
            <ul class="list-group list-group-horizontal">
                <li class="list-group-item list-group-item-danger">有钱不花,掉了白搭</li>
                <li class="list-group-item list-group-item-warning">钱是王八蛋,没了咱再赚</li>
                <li class="list-group-item list-group-item-info">每天不是买买买,就是去买买买的路上</li>
            </ul>
        </div>
    </header>
    <main>
        <div class="text-center">
            <h1 class="display-4 fw-normal text-info" th:text="${msg}"/>
        </div>
    </main>
</div>
</body>
</html>
  • css 内容(pricing.css)
代码语言:javascript
复制
body {
  background-image: linear-gradient(180deg, #eee, #fff 100px, #fff);
}

.container {
  max-width: 960px;
}

.pricing-header {
  max-width: 700px;
}
  • 准备图片资源(gfxm.jpg[功夫熊猫]、wzt.jpg[威震天]、xhr.jpg[小黄人闹翻天])

至此,caicaishop 项目的整体结构变的感觉有点像样了。

4. 启动项目,看看效果

4.1. 体验商品购买

运行 CaiCaiShopApplication 项目入口,直接访问店铺,购买体验顺畅。

至此,基于 Spring Boot 搭建的照片(骗)售卖店铺就完成了,菜菜正计划对外推广。

不过,菜菜同学不是一个省油的灯,而且会点前端,准确说也是个前端的二把刀,于是想整个脚本看看网站的并发处理能力。

于是菜菜花了一根烟的功夫用 HTML 编写了一个模拟并发的页面。

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>高并发</title>
    <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
</head>
<body>
<div class="text-center">
    <h1 class="display-4 fw-normal text-info"/>
</div>
<script type="text/javascript">
    const userId = 10086;
    const goodsId = 3;
    const times = 1500;
    for (i = 1; i <= times; i++) {
        $.ajax({
            type: "GET",
            url: "http://localhost:8080/api/purchase",
            data: "userId=" + userId + "&goodsId=" + goodsId,
            async: true,
            success: function (msg) {
                console.log(msg);
            }
        });

        if (i === times) {
            $(".text-center").text(times + "次请求发送完毕");
        }
    }
</script>
</body>
</html>

文件保存成 .html,然后用浏览器直接打开,发现坏事儿了。

4.2. 坏事儿了

当用浏览器直接打开菜菜的 html 并发脚本,发现服务端日志偶尔会出现了超卖,控制台输出如下。

数据库发现库存变成负数 。

看到这种效果,菜菜泪奔,本来想拿照片(骗)换点银子,这么下去不得亏大发呀,那该怎么办?各自先自行查查是咋回事?本次不做解答,下次一起揭秘。

5. 例行回顾

本文开始正式迈入 Spring Boot 应用篇,主要是通过项目搭建来熟练前期讲过的 Spring Boot 相关集成技术,随着后续的逐步深入,会一起探究一下悲观锁、乐观锁、秒杀相关的缓存、削峰等相关知识

,看看如何借助这些知识点一步一步来解决业务问题。

生活就是不断突破自我的过程。我们努力地向上,不仅是让世界看到我们,更是为了让自己看到世界。当我们一步一个脚印往前走时会发现,每一点进步,都在让我们的人生变得辽阔。

参考资料:

https://spring.io/

https://start.spring.io/

https://spring.io/projects/spring-boot

https://github.com/spring-projects/spring-boot

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/

https://stackoverflow.com/questions/tagged/spring-boot

《Spring Boot实战》《深入浅出Spring Boot 2.x》

《一步一步学Spring Boot:微服务项目实战(第二版)》

《Spring Boot揭秘:快速构建微服务体系》

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-04-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一猿小讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档