本书源码
https://github.com/huangwenyi10/spring-boot-book-v2
目录
- 创建
- 秒杀系统
- 雏形
- 小结
- 详情
- 高并发优化
- 流量削峰
创建
解决 Cannot resolve plugin org.apache.maven.plugins:maven-site-plugin:3.8.2:
https://ld246.com/article/1584103058009
秒杀系统
雏形
源码:
https://github.com/13407196713/SpringBoot2
小结
创建数据库speed-kill-system
三个表 ay_product、ay_user、ay_user_kill_product
分别是用户、商品、秒杀的商品
model/AyUser
model/AyProduct
model/AyUserKillProduct
model/KillStatus
mysql的依赖和设置
依赖:
application.properties配置:
repository/ProductRepository 商品的增删改查 CRUD
repository/AyUserKillProductRepository 用户秒杀商品记录Repository 增删查改 CRUD
service/ProductService
service/AyUserKillProductService
service/impl/ProductServiceImpl
service/impl/AyUserKillProductServiceImpl
controller/ProductController
前端依赖和配置
依赖:
application.properties配置:
templates/product_list.html
templates/success.html
templates/fail.html
详情
创建数据库speed-kill-system
三个表 ay_product、ay_user、ay_user_kill_product
分别是用户、商品、秒杀的商品
model/AyUser
// 用户
// 对实体注释。任何 Hibernate 映射对象都要有这个注释
@Entity
// 声明此对象映射到数据库的数据表,如果没有则系统使用默认值(实体的短类名)。
@Table(name = "ay_user")
public class AyUser implements Serializable {
//主键
@Id
private Integer id;
//用户名
private String name;
//电话号码
private String phoneNumber;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
}
model/AyProduct
// 商品
@Entity
@Table(name = "ay_product")
public class AyProduct implements Serializable {
/**
* 商品id
*/
@Id
private Integer id;
/**
* 商品图片
*/
private String productImg;
/**
* 商品名称
*/
private String name;
/**
* 商品数量
*/
private Integer number;
/**
* 秒杀开始时间
*/
private Date startTime;
/**
* 秒杀结束时间
*/
private Date endTime;
/**
* 创建时间
*/
private Date createTime;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
public Date getStartTime() {
return startTime;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public Date getEndTime() {
return endTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public String getProductImg() {
return productImg;
}
public void setProductImg(String productImg) {
this.productImg = productImg;
}
}
model/AyUserKillProduct
// 秒杀商品
@Entity
@Table(name = "ay_user_kill_product")
public class AyUserKillProduct implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
//指定主键的生成策略。有如下四个值 id是主键
// TABLE:使用表保存id值
// IDENTITY:由数据库自动生成
// SEQUENCR :根据底层数据库的序列来生成主键,条件是数据库支持序列
// AUTO:主键由程序控制
private Integer id;
/**
* 商品id
*/
private Integer productId;
/**
* 用户id
*/
private Integer userId;
/**
* 状态,-1:无效;0:成功;1:已付款'
*/
private Integer state;
/**
* 创建时间
*/
private Date createTime;
public Integer getProductId() {
return productId;
}
public void setProductId(Integer productId) {
this.productId = productId;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public Integer getState() {
return state;
}
public void setState(Integer state) {
this.state = state;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
}
model/KillStatus
// 描述:秒杀状态类
public enum KillStatus {
IN_VALID(-1, "无效"),
SUCCESS(0, "成功"),
PAY(1,"已付款");
private int code;
private String name;
KillStatus(){}
KillStatus(int code, String name){
this.code = code;
this.name = name;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
mysql的依赖和设置
依赖:
<!-- mysql start -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
application.properties配置:
### mysql连接信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/speed-kill-system?serverTimezone=UTC
###用户名
spring.datasource.username=root
###密码
spring.datasource.password=123456
###驱动
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
repository/ProductRepository 商品的增删改查 CRUD
// 商品的增删改查 CRUD
public interface ProductRepository extends JpaRepository<AyProduct,Integer> {
}
repository/AyUserKillProductRepository 用户秒杀商品记录Repository 增删查改 CRUD
// 描述:用户秒杀商品记录Repository 增删查改 CRUD
public interface AyUserKillProductRepository extends JpaRepository<AyUserKillProduct,Integer> {
}
service/ProductService
public interface ProductService {
//查询所有商品
List<AyProduct> findAll();
//查询所有商品
// Collection<AyProduct> findAllCache();
// 秒杀商品
// @param productId 商品id
// @param userId 用户id
AyProduct killProduct(Integer productId, Integer userId);
}
service/AyUserKillProductService
//描述:用户秒杀商品记录接口
public interface AyUserKillProductService {
//保存用户秒杀商品记录
AyUserKillProduct save(AyUserKillProduct killProduct);
}
service/impl/ProductServiceImpl
// 商品服务
@Service
public class ProductServiceImpl implements ProductService {
@Resource
private ProductRepository productRepository;
@Resource
private AyUserKillProductService ayUserKillProductService;
//日志
Logger logger = LoggerFactory.getLogger(ProductServiceImpl.class);
// 查询所有商品
@Override
public List<AyProduct> findAll() {
try{
List<AyProduct> ayProducts = productRepository.findAll();
return ayProducts;
}catch (Exception e){
logger.error("ProductServiceImpl.findAll error", e);
return Collections.EMPTY_LIST;
}
}
// 秒杀商品
// @param productId 商品id
// @param userId 用户id
@Override
public AyProduct killProduct(Integer productId, Integer userId) {
//查询商品
AyProduct ayProduct = productRepository.findById(productId).get();
//判断商品是否还有库存
if(ayProduct.getNumber() < 0){
return null;
}
//设置商品的库存:原库存数量 - 1
ayProduct.setNumber(ayProduct.getNumber() - 1);
//更新商品库存
ayProduct = productRepository.save(ayProduct);
//保存商品的秒杀记录
AyUserKillProduct killProduct = new AyUserKillProduct();
killProduct.setCreateTime(new Date());
killProduct.setProductId(productId);
killProduct.setUserId(userId);
//设置秒杀状态
killProduct.setState(KillStatus.SUCCESS.getCode());
//保存秒杀记录详细信息
ayUserKillProductService.save(killProduct);
//商品秒杀成功后,更新缓存中商品库存数量
// redisTemplate.opsForHash().put(KILL_PRODUCT_LIST, killProduct.getProductId(),ayProduct);
return ayProduct;
}
}
service/impl/AyUserKillProductServiceImpl
@Service
public class AyUserKillProductServiceImpl implements AyUserKillProductService {
@Resource
private AyUserKillProductRepository ayUserKillProductRepository;
//保存用户秒杀商品记录
//@param killProduct
@Override
public AyUserKillProduct save(AyUserKillProduct killProduct) {
return ayUserKillProductRepository.save(killProduct);
}
}
controller/ProductController
@Controller
@RequestMapping("/products")
public class ProductController {
@Resource
private ProductService productService;
//查询所有的商品
@RequestMapping("/all")
public String findAll(Model model){
List<AyProduct> products = productService.findAll();
model.addAttribute("products", products);
return "product_list";
}
// 秒杀商品
@RequestMapping("/{id}/kill")
public String killProduct(Model model,
@PathVariable("id") Integer productId,
@RequestParam("userId") Integer userId){
AyProduct ayProduct = productService.killProduct(productId, userId);
if(null != ayProduct){
return "success";
}
return "fail";
}
}
前端依赖和配置
依赖:
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
application.properties配置:
#thymeleaf配置
#模板的模式,支持如:HTML、XML、TEXT、JAVASCRIPT等
spring.thymeleaf.mode=HTML5
#编码,可不用配置
spring.thymeleaf.encoding=UTF-8
#内容类别,可不用配置
spring.thymeleaf.servlet.content-type=text/html
#开发配置为false,避免修改模板还要重启服务器
spring.thymeleaf.cache=false
#配置模板路径,默认就是templates,可不用配置
#spring.thymeleaf.prefix=classpath:/templates/
templates/product_list.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- 引入 Bootstrap -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<!-- 页面显示部分-->
<div class="container">
<div class="panel panel-default">
<div class="panel-heading text-center">
<h1>秒杀活动</h1>
</div>
<div class="panel-body">
<table class="table table-hover">
<thead>
<tr>
<td>图片</td>
<td>名称</td>
<td>库存</td>
<td>开始时间</td>
<td>结束时间</td>
<td>操作</td>
</tr>
</thead>
<tbody>
<tr th:each="product:${products}">
<td>
<img border="1px" width="100px" height="110px" th:src="@{${product.productImg}}"/>
</td>
<td th:text="${product.name}"></td>
<td th:text="${product.number}"></td>
<td th:text="${product.startTime}"></td>
<td th:text="${product.endTime}"></td>
<td>
<!-- 发起秒杀请求,用户id先简单写死为 1 -->
<a class="btn btn-info" th:href="@{'/products/'+${product.id} + '/kill' + '?userId=1'}">秒杀</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</body>
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</html>
templates/success.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div align="center">
客官!!! 恭喜您,秒杀成功~~~
</div>
</body>
</html>
templates/fail.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div align="center">
不要灰心,继续加油~~~
</div>
</body>
</html>
启动 SpringBoot2Application
进入 http://localhost:8080/products/all
高并发优化
源码
https://github.com/13407196713/SpringBoot2_2
引入redis缓存
application.properties
### redis缓存配置
### 默认redis数据库为db0
spring.redis.database=0
### 服务器地址,默认为localhost
spring.redis.host=localhost
### 链接端口,默认为6379
spring.redis.port=6379
### redis密码默认为空
spring.redis.password=
spring.redis.timeout=10000
service/ProductService
//查询所有商品
Collection<AyProduct> findAllCache();
service/impl/ProductServiceImpl
//注入redisTemplate对象
@Resource
private RedisTemplate redisTemplate;
//定义缓存key
private static final String KILL_PRODUCT_LIST = "kill_product_list";
//查询商品数据(带缓存)
@Override
public Collection<AyProduct> findAllCache() {
try{
//从缓存中查询商品数据
Map<Integer, AyProduct> productMap = redisTemplate.opsForHash().entries(KILL_PRODUCT_LIST);
Collection<AyProduct> ayProducts = null;
//如果缓存中查询不到商品数据
if(CollectionUtils.isEmpty(productMap)){
//从数据库中查询商品数据
ayProducts = productRepository.findAll();
//将商品list转换为商品map
productMap = convertToMap(ayProducts);
//将商品数据保存到缓存中
redisTemplate.opsForHash().putAll(KILL_PRODUCT_LIST, productMap);
//设置缓存数据的过期时间,这里设置10s,具体时间需要结合业务需求而定
//如果商品数据变化少,过期时间可以设置长一点;反之,过期时间可以设置短一点
redisTemplate.expire(KILL_PRODUCT_LIST,10000 , TimeUnit.MILLISECONDS);
return ayProducts;
}
ayProducts = productMap.values();
return ayProducts;
}catch (Exception e){
logger.error("ProductServiceImpl.findAllCache error", e);
return Collections.EMPTY_LIST;
}
}
//list转换为map
private Map<Integer, AyProduct> convertToMap(Collection<AyProduct> ayProducts){
if(CollectionUtils.isEmpty(ayProducts)){
return Collections.EMPTY_MAP;
}
Map<Integer, AyProduct> productMap = new HashMap<>(ayProducts.size());
for(AyProduct product: ayProducts){
productMap.put(product.getId(), product);
}
return productMap;
}
controller/ProductController
// 查询所有的商品(缓存)
@RequestMapping("/all/cache")
public String findAllCache(Model model){
Collection<AyProduct> products = productService.findAllCache();
model.addAttribute("products", products);
return "product_list";
}
商品秒杀成功后,更新缓存中商品库存数量
service/impl/ProductServiceImpl
//商品秒杀成功后,更新缓存中商品库存数量
redisTemplate.opsForHash().put(KILL_PRODUCT_LIST, killProduct.getProductId(),ayProduct);
启动 redis的redis-server.exe、redis-cli.exe
启动程序
流量削峰
源码
https://github.com/13407196713/SpringBoot2_3
为了让并发请求更平缓,我们需要流量削峰
用消息队列缓冲瞬时流量
ActiveMQ下载安装
https://blog.csdn.net/weixin_38361347/article/details/83796570
ActiveMQ 依赖
<!-- activemq start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
配置
### activemq 配置
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.in-memory=true
spring.activemq.pool.enabled=false
spring.activemq.packages.trust-all=true
生产者
producer/AyProductKillProducer
// 生产者
@Service
public class AyProductKillProducer {
//日志
Logger logger = LoggerFactory.getLogger(ProductServiceImpl.class);
@Resource
private JmsMessagingTemplate jmsMessagingTemplate;
/**
* 描述:发送消息
* @param destination 目标地址
* @param killProduct 描述商品
*/
public void sendMessage(Destination destination, final AyUserKillProduct killProduct) {
logger.info("AyProductKillProducer sendMessage , killProduct is" + killProduct);
jmsMessagingTemplate.convertAndSend(destination, killProduct);
}
}
消费者
consumer/AyProductKillConsumer
// 消费者
@Component
public class AyProductKillConsumer {
//日志
Logger logger = LoggerFactory.getLogger(ProductServiceImpl.class);
@Resource
private AyUserKillProductService ayUserKillProductService;
//消费消息
@JmsListener(destination = "ay.queue.asyn.save")
public void receiveQueue(AyUserKillProduct killProduct){
//保存秒杀商品数据
ayUserKillProductService.save(killProduct);
//记录日志
logger.info("ayUserKillProductService save, and killProduct: " + killProduct);
}
}
修改 service/impl/ProductServiceImpl
@Resource
private AyProductKillProducer ayProductKillProducer;
//队列
private static Destination destination = new ActiveMQQueue("ay.queue.asyn.save");
/**
* 秒杀商品(引入MQ)
* @param productId 商品id
* @param userId 用户id
*/
@Override
public AyProduct killProduct(Integer productId, Integer userId) {
//查询商品
AyProduct ayProduct = productRepository.findById(productId).get();
//判断商品是否还有库存
if(ayProduct.getNumber() < 0){
return null;
}
//设置商品的库存:原库存数量 - 1
ayProduct.setNumber(ayProduct.getNumber() - 1);
//更新商品库存
ayProduct = productRepository.save(ayProduct);
//保存商品的秒杀记录
AyUserKillProduct killProduct = new AyUserKillProduct();
killProduct.setCreateTime(new Date());
killProduct.setProductId(productId);
killProduct.setUserId(userId);
//设置秒杀状态
killProduct.setState(KillStatus.SUCCESS.getCode());
//保存秒杀记录详细信息
//ayUserKillProductService.save(killProduct);
//异步保存商品的秒杀记录
ayProductKillProducer.sendMessage(destination, killProduct);
//商品秒杀成功后,更新缓存中商品库存数量
redisTemplate.opsForHash().put(KILL_PRODUCT_LIST, killProduct.getProductId(),ayProduct);
return ayProduct;
}
启动 redis的redis-server.exe、redis-cli.exe
启动apache-activemq 的 bin/win64/activemq
启动程序