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. 设计数据库表

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

  • pom.xml 加入依赖

<!-- 引入 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 文件中添加配置信息。

# 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 实体类

/*** 商品信息类*/
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 实体类

/*** 用户商品购买记录*/
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)

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

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)

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

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

<?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

<?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)

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)

package org.growup.caicaishop.service;import org.growup.caicaishop.entity.UserGoods;public interface UserGoodsService {/*** 保存用户购买记录*/public int save(UserGoods userGoods);
}
  • 处理购买业务的Service(PurchaseService)

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

1.8. 创建 Service 接口的实现类

  • 商品Service实现类(GoodsServiceImpl)

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 {@Resourceprivate GoodsDao goodsDao;@Overridepublic List<Goods> findAllGoods() {return goodsDao.selectAll();}@Overridepublic int reduceStock(Integer goodsId,int quantity) {return goodsDao.reduceStock(goodsId,quantity);}
}
  • 用户购买商品记录Service实现类(UserGoodsServiceImpl)

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 {@Resourceprivate UserGoodsDao userGoodsDao;@Overridepublic int save(UserGoods userGoods) {return userGoodsDao.insert(userGoods);}
}
  • 购买逻辑处理Service实现类(PurchaseServiceImpl)

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");@Resourceprivate GoodsDao goodsDao;@Resourceprivate UserGoodsDao userGoodsDao;@Override@Transactionalpublic 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");@Autowiredprivate GoodsService goodsService;@Autowiredprivate UserGoodsService userGoodsService;@Autowiredprivate PurchaseService purchaseService;@Testpublic void testFindAllGoods() {List<Goods> goodsList = goodsService.findAllGoods();logger.info("商品信息:" + goodsList);}@Testpublic 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));}@Testpublic void testPurchase() {boolean purchaseRes = purchaseService.purchase(1,1,1);logger.info("商品购买结果:" + purchaseRes);}
}
  • 验证 GoodService 中获取商品列表功能。

  • 验证 UserGoodsService 中保存用户购买商品记录功能。

  • 验证 PurchaseService 中商品购买功能。

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

2. 控制层实现

  • 首页控制器(IndexController)

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 {@Resourceprivate GoodsService goodsService;@RequestMapping({"","/","/index"})public String index(Model model) {model.addAttribute("goodsList", goodsService.findAllGoods());return "index";}
}

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

  • 商品购买控制器(PurchaseController)

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 {@Resourceprivate 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 依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • thymeleaf 相关配置

在 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 展示层实现

  • 店铺首页(index.html)

<!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)

<!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)

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 编写了一个模拟并发的页面。

<!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揭秘:快速构建微服务体系》

玩转 Spring Boot 应用篇(搭建菜菜的店铺)相关推荐

  1. 玩转spring boot——开篇

    很久没写博客了,而这一转眼就是7年.这段时间并不是我没学习东西,而是园友们的技术提高的非常快,这反而让我不知道该写些什么.我做程序已经有十几年之久了,可以说是彻彻底底的"程序老炮" ...

  2. spring boot:从零开始搭建一个项目 - day 4 控制台输出日志美化 + swagger2

    spring boot:从零开始搭建一个项目 - day 4 控制台输出日志美化 + swagger2 一.logback.xml配置日志美化 二.集成swagger2 1.引入依赖 2.编写配置文件 ...

  3. 玩转spring boot——结合阿里云持续交付

    前言 在互联网项目中,项目测试.部署往往需要花费大量时间.传统方式是在本地打包.测试完毕程序,然后通过ftp上传至服务器,再把测试的配置文件修改为生产环境的配置文件,最后重新运行服务.这一过程如果交给 ...

  4. 玩转spring boot——结合redis

    前言 Redis 是一个高性能的key-value数据库. redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用.它提供了 ...

  5. Spring Boot Restful框架搭建和使用【持续更新中】

    2019独角兽企业重金招聘Python工程师标准>>> 1 Spring Boot Restful框架搭建 项目源码地址 2 SpringBoot框架使用技巧记录 2.1 pagea ...

  6. Spring boot Gradle项目搭建

    Spring boot Gradle项目搭建 使用IDEA创建Gradle工程     操作大致为:File->new->Project->Gradle(在左侧选项栏中)     创 ...

  7. 玩转spring boot——国际化

    前言 在项目开发中,可能遇到国际化的问题,而支持国际化却是一件很头疼的事.但spring boot给出了一个非常理想和方便的方案. 一.准备工作 pom.xml: <?xml version=& ...

  8. 玩转spring boot——ajax跨域

    前言  java语言在多数时,会作为一个后端语言,为前端的php,node.js等提供API接口.前端通过ajax请求去调用java的API服务.今天以node.js为例,介绍两种跨域方式:Cross ...

  9. 玩转spring boot——结合docker

    前言 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 liunx机器上,也可以实现虚拟化.容器是完全使用沙箱机制,相互之间不会有 ...

最新文章

  1. U盘打不开的解决方法
  2. 逆势而上的技术:图神经网络学习来了!
  3. 三层交换(VLAN间互通+路由功能)+VTP+STP(PVST)综合实验(理论+实践=真实)
  4. 1)phpmyadmin导入数据库大小限制修改
  5. 【剑指offer - C++/Java】13、调整数组顺序使奇数位于偶数前面
  6. 华为交换机telnet和ftp服务开启/关闭命令
  7. thinkpad linux 键盘,Linux 5.10或支持联想PC键盘快捷键
  8. 概率图模型笔记(一)开坑
  9. hibernate中基于主键映射1-1关联关系和基于外键映射1-1关联关系的不同
  10. 删缓存,数据库更新谁先执行,及延时双删
  11. 机器学习系列(5)_从白富美相亲看特征预处理与选择(上)
  12. 一篇Word论文的字体格式是什么?
  13. extension(扩展)使用详情
  14. Block insecure private network requests
  15. linux与pe到移动硬盘,PE下找不到移动硬盘?不用怕
  16. 【论文阅读】8-Non-local Scan Consolidation for 3D Urban Scenes
  17. 第12节 实例-挖方填方量的计算
  18. hadoop生态圈的理解
  19. 计算机课上最难忘的一幕
  20. ctab法提取dna流程图_CTAB法提取DNA原理及步骤、制胶、电泳

热门文章

  1. miui12怎么自定义开机动画_触碰想象,感受真实!一文看懂MIUI12,超40款机型适配...
  2. JavaWeb-Springboot图片裁剪
  3. Filter学习笔记
  4. 用Python解小学数学题(人教版二年级(上)第35页)
  5. 微信公众平台—— 获取微信服务器IP地址
  6. U盘连接Win10系统电脑后不显示的解决方法
  7. @echo off 的作用
  8. 头歌23根火柴的小游戏
  9. Xshell连接卡顿输入很慢甚至无反应
  10. 如何与 Target 塔吉特建立EDI连接?