0.
0.0. 历史文章整理
玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)
玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)
玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)
玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)
玩转 Spring Boot 集成篇(定时任务框架Quartz)
玩转 Spring Boot 原理篇(自动装配前凑之自定义Starter)
玩转 Spring Boot 原理篇(内嵌Tomcat实现原理&优雅停机源码剖析)
0.1. 背景
菜菜同学前几天与好友一起去环球影城溜达了一天,亲自拍了一系列富有感情的珍藏版照片。为了能更好的输出高质量的技术文章,菜菜想要把照片挂到网站上卖掉来换点银子,然后把写作装备更新一波。
不过,菜菜同学需要快速搭建一个商品售卖网站(菜菜的店铺),以便能够把照片尽快卖掉换点银子 ... ...
未曾想建设店铺途中,遇到了超卖、高并发以及瞬间过高的请求导致访问高峰等一系列的问题,不过终究都被菜菜给化解啦,咱们后面慢慢去谈。
0.2. 技术选型
0.3. 项目演示
1. 从 0 开始,动手操练起来。
1.1. 设计数据库表
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 依赖包)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
项目名为 caicaishop。
项目创建后,直接运行 main 函数,看看环境是否 OK,正常启动会输出。
1.3. 集成 MyBatis
<!-- 引入 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>
在 application.properties 文件中添加配置信息。
# 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 目录,用于存放实体类源代码文件。
/**
* 商品信息类
*/
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 方法
}
/**
* 用户商品购买记录
*/
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
提供查询单个商品、查询所有商品以及减库存的方法定义。
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);
}
提供保存用户购买记录的方法定义。
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 文件。
<?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>
<?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 接口
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);
}
package org.growup.caicaishop.service;
import org.growup.caicaishop.entity.UserGoods;
public interface UserGoodsService {
/**
* 保存用户购买记录
*/
public int save(UserGoods userGoods);
}
package org.growup.caicaishop.service;
public interface PurchaseService {
/**
* 处理购买业务
*/
public boolean purchase(Integer userId, Integer goodsId, int quantity);
}
1.8. 创建 Service 接口的实现类
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);
}
}
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);
}
}
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. 单元测试验证
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);
}
}
至此,数据库层面的 CRUD 封装完成,验证通过。
2. 控制层实现
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 视图。
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)的完全替代品,并实现自然模板的概念:模板文件可以直接在浏览器中打开,并且仍然可以正确显示为网页。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
在 application.properties 文件中加入如下配置。
### 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 展示层实现
<!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>
<!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>
body {
background-image: linear-gradient(180deg, #eee, #fff 100px, #fff);
}
.container {
max-width: 960px;
}
.pricing-header {
max-width: 700px;
}
至此,caicaishop 项目的整体结构变的感觉有点像样了。
4. 启动项目,看看效果
4.1. 体验商品购买
运行 CaiCaiShopApplication 项目入口,直接访问店铺,购买体验顺畅。
至此,基于 Spring Boot 搭建的照片(骗)售卖店铺就完成了,菜菜正计划对外推广。
不过,菜菜同学不是一个省油的灯,而且会点前端,准确说也是个前端的二把刀,于是想整个脚本看看网站的并发处理能力。
于是菜菜花了一根烟的功夫用 HTML 编写了一个模拟并发的页面。
<!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揭秘:快速构建微服务体系》