SpringBoot from entry to Mastery Series 8: Practice of high concurrency seckill system

Taking the common seckill scenarios on e-commerce websites as an example, SpringBoot integrates Spring MVC, Mybatis, Redis, RabbitMQ and other technologies to realize a high concurrency seckill system.

The system includes commonly used SSM technology, Redis based distributed Session, Redis based data cache, Redis based page cache, RabbitMQ based second kill current limiting, etc.

1, System architecture

1. System architecture

Based on the development of SpringBoot, the main function of SpringBoot is to provide automatic configuration for integrating various frameworks. In fact, Spring MVC, Spring, MyBatis, Redis, RabbitMQ and other technologies still work.

The system uses Thymeleaf as the view template technology and jQuery as the JS tool library to dynamically update the page.

The system adopts strict Java EE application structure, mainly including the following layers:

  • Presentation layer: composed of Thymeleaf pages
  • MVC layer: using Spring MVC framework
  • Message component layer: Implementation Based on RabbitMQ
  • Page cache layer: managed by Redis
  • Business logic layer: mainly composed of business logic components managed by Spring IoC container
  • Data cache layer: managed by Redis
  • DAO layer: composed of MyBatis Mapper component
  • Domain object layer: domain objects are responsible for mapping with the result set
  • Database service layer: use MySQL database to store persistent data

2. Related technologies

MVC framework:

  • All user requests of the system, including system hyperlinks and form submission, are no longer sent directly to the presentation layer page, but must be sent to the processing method of the Spring MVC controller, which controls the processing and forwarding of all requests.
  • The system uses the permission control based on Spring MVC interceptor. The controller in the application does not check the permission, but each controller needs to repeatedly check whether the caller has sufficient access rights. This general operation is the use of the interceptor.
  • The whole application has two roles: ordinary employees and managers. You only need to configure different interceptors for these two roles in the configuration file of Spring MVC to fully check the permissions of ordinary employees and managers.

The role of Spring framework:

  • The Spring framework is the core of the system. The IoC container provided by Spring is the factory of business logic components and DAO components, which is responsible for generating and managing these instances.
  • SpringBoot is also built based on the IoC and AOP functions of the Spring framework.
  • With the help of Spring's dependency injection, various components are combined in a loosely coupled manner, and the dependencies between components are managed through Spring's dependency injection.
  • Both Service components and DAO objects adopt interface oriented programming, which reduces the cost of system reconfiguration and greatly improves the maintainability and modifiability of the system.
  • Application transactions adopt Spring's declarative transaction framework. Through declarative transactions, the transaction strategy does not need to be coupled with the code in a hard coded way, but is declared in the configuration file, so that the business logic components can focus more on the implementation of the business, thus simplifying the development. Declarative transaction reduces the switching cost of different transaction strategies.

Role of MyBatis:

  • MyBatis is used as a SQL Mapping framework. The SQL Mapping function simplifies database access and provides better encapsulation in the JDBC layer.
  • As a semi-automatic SQL Mapping framework, MyBatis simplifies the development steps of Mapper components (DAO components). As long as developers define Mapper interfaces and write SQL statements for CRUD methods in the interfaces through XML Mapper configuration files or annotations, MyBatis will automatically generate implementation classes for Mapper interfaces.
  • The semi-automatic MyBatis framework can not only simplify the development of DAO layer components, but also allow developers to manually write SQL statements. Therefore, developers can make full use of their SQL knowledge, write simple and flexible SQL statements, optimize SQL statements and improve program performance.

Redis's role:

  • Redis has two main functions in this system:
  • Managing distributed sessions: because this highly concurrent seckill system is usually a distributed application, the traditional stand-alone Web Session is not applicable. Redis will be used to manage distributed sessions.
  • Caching: the system not only caches the data that needs frequent access in the database, but also directly caches the static pages that need frequent access with few changes. Directly caching the static pages can greatly improve the response speed of applications in the face of high concurrency.

RabbitMQ's role:

  • RabbitMQ's main function is to slit the instantaneous and highly concurrent second kill requests: when the second kill request arrives, after the controller's processing method receives the second kill request, the controller does not directly call the method of the Service component to process the second kill request, but simply sends a message to the RabbitMQ message queue.
  • A message consumer is also defined in the application. The message consumer will read the messages in the RabbitMQ message queue one by one according to its rhythm. Each time a message is read, it will process a second kill request.
  • After such processing, regardless of the number of instantaneous concurrent requests from the client, the processing method of the controller just receives these requests, adds these requests to the RabbitMQ message queue in a first come first served manner, and then allows the message consumer to slowly process these instantaneous high concurrent requests one by one.

3. System function module

The system can be roughly divided into two modules:

  • User module
  • Second kill module

Business logic is implemented through two business logic components, UserService and MiaoshaService, which are used to encapsulate Mapper components (DAO components)

The system takes business logic components as the facade of Mapper components and encapsulates these Mapper components. The bottom layer of business logic components depends on these Mapper components to realize the business logic functions of the system upward.

The system mainly has the following four Mapper objects:

  • UserMapper: provides user_ Basic operations of inf table
  • MiaoshaItemMapper: provide item_inf table and Miaosha_ Basic operation of item table
  • OrderMapper: provides a description of the order_ Basic operations of inf table
  • MiaoshaOrderMapper: provides the support for order_inf table and Miaosha_ Basic operation of order.

The system provides the following two business logic components:

  • UserService: provides the implementation of business logic functions such as user login and user information viewing
  • MiaoshaService: provides the realization of logical functions such as commodity viewing and commodity second kill.

The system provides the following two message components:

  • MiaoshaSender: this component is used to send messages to the RabbitMQ message queue
  • MiaoshaReceiver: this component is used to receive messages from RabbitMQ message queue

The system also provides a Redis operation component: fkretisutil, which is implemented based on Spring Data Redis

2, Project construction

The system will use the following frameworks and technologies:

  • Spring MVC: provided by SpringBoot Web
  • Thymeleaf: provided by SpringBoot Thymeleaf
  • MyBatis: provided by MyBats SpringBoot. You need to access MySQL database, so you also need to add MySQL driver library.
  • Redis: provided by SpringBoot Data Redis. When connecting to redis, you also need to rely on Apache Commons Pool2 connection pool
  • RabbitMQ: provided by SpringBoot AMQP
  • The system also uses two tool libraries: common Lang 3 and common codec. Common Lang 3 provides a large number of tool classes such as StringUtils, ArrayUtils, ClassUtils and RegexUtils. It is more convenient to use the static methods provided by these tool classes. Common codec contains some general encoding and decoding algorithms, such as MD5 encryption algorithm to be used in this system.

1. Create pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<!-- Specify inheritance spring-boot-starter-parent POM file -->
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.2</version>
		<relativePath/>
	</parent>

	<groupId>org.crazyit</groupId>
	<artifactId>miaosha</artifactId>
	<version>1.0-SNAPSHOT</version>
	<name>miaosha</name>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>11</java.version>
	</properties>

	<dependencies>
		<!-- Spring Boot Web -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- Spring Boot Thymeleaf -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<!-- Spring Boot AMQP -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
		<!-- Spring Boot Data Redis -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<!-- add to Apache Commons Pool2 Dependence of -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
			<version>2.9.0</version>
		</dependency>
		<!-- MyBatis Spring Boot -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.4</version>
		</dependency>
		<!-- MySQL drive -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<!-- commons-lang3 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.11</version>
		</dependency>
		<!-- common-codec -->
		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
			<version>1.15</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<!-- definition Spring Boot Maven Plug in, which can be used to run Spring Boot application -->
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

3, Domain object layer

By using MyBatis to persist domain objects, you can avoid using the traditional JDBC method to operate the database.

By using the SQL Mapping support provided by MyBatis, the program is allowed to operate the relational database in an object-oriented manner, which ensures that the software development process is carried out in an object-oriented manner, that is, object-oriented analysis, object-oriented design and object-oriented programming.

1. Design field objects

Object oriented analysis refers to extracting objects in applications according to system requirements, abstracting these objects into classes, and then extracting classes that need to be persisted. These classes that need to be persisted are domain objects.

The system does not design the database in advance, but completely starts from object-oriented analysis and designs the following domain object classes.

The system includes the following five domain object classes.

  • User: the user corresponding to the seckill system
  • Item: corresponds to the commodity in the second kill system, including the name, description and other basic information of the commodity
  • MiaoshaItem: corresponds to the goods participating in the second kill. In addition to the basic commodity information, it also includes the second kill price, inventory, start time and end time of the second kill.
  • Order: corresponding to the order information, it is used to save the order user, order price, order time and other necessary information
  • MiaoshaOrder: corresponding to the second kill order, only the user ID, order ID, commodity ID and other basic information are saved

From the perspective of database, the data tables corresponding to the above five domain object classes are associated.

user_ Create table statement of inf table:

-- Second kill user watch
drop table if exists user_inf;
create table user_inf
(
  user_id bigint primary key comment 'Mobile number as user ID',
  nickname varchar(255) not null,
  password varchar(32) comment 'Save the encrypted password after adding salt: MD5(password, salt)',
  salt varchar(10),
  head varchar(128) comment 'Avatar address',
  register_date datetime comment 'Registration time',
  last_login_date datetime comment 'Last login time',
  login_count int comment 'Login times'
) comment='Second kill user watch';
insert into user_inf values
(13500008888, 'fkjava', '7fb8f847c09ad032fbf3e3b9fcd2101f', '0p9o8i', null, curdate(), curdate(), 1),
(13500006666, 'fkit', '7fb8f847c09ad032fbf3e3b9fcd2101f', '0p9o8i', null, curdate(), curdate(), 1),
(13500009999, 'crazyit', '7fb8f847c09ad032fbf3e3b9fcd2101f', '0p9o8i', null, curdate(), curdate(), 1);

item_ Create table statement of inf table:

--  Commodity list
drop table if exists item_inf;
create table item_inf
(
  item_id bigint primary key auto_increment comment 'commodity ID',
  item_name varchar(255) comment 'Trade name',
  title varchar(64) comment 'Product title',
  item_img varchar(64) comment 'Pictures of goods',
  item_detail longtext comment 'Product details',
  item_price decimal(10,2) comment 'item pricing ',
  stock_num int comment 'Commodity inventory,-1 Indicates that there are no restrictions'
) comment='Commodity list';
insert into item_inf values
(1, 'Java handout', 'Sell hundreds of thousands of copies, become the choice of readers on both sides of the Taiwan Strait, and give 20 free copies+Hour video, source code, courseware, interview questions, wechat communication Q & a group', 'books/java.png', '1)The author provides supporting websites for learning and communication, and the author's personal online wechat group QQ Group.<br>2)<insane Java The lecture notes took ten years to precipitate, and has been upgraded to the fifth edition after countless years Java The repeated verification of learners has been introduced as reference materials and selected as teaching materials by a large number of excellent teachers from 985 and 211 universities, including Peking University.<br>3)<insane Java The handout has been translated into traditional Chinese characters and has been listed and issued in Taiwan.<br>4)<insane Java Handout has won many awards, won the "best seller" and "long selling book" Awards of electronic industry press for many times, and the author himself has won the title of "excellent author" for many times. The printing volume of the third edition alone reached more than 90000 copies.', 139.00, 2000);
insert into item_inf values
(2, 'Lightweight Java Web Enterprise application practice—— Spring MVC+Spring+MyBatis Integrated development', 'Source level analysis Spring Frame, suitable for mastered Java Basic or finished crazy Java For the reader of the handout, send the supporting code and 100 minute course. Enter wechat group', 'books/javaweb.png', '<Lightweight Java Web Enterprise application practice――Spring MVC+Spring+MyBatis Integrated development is not a“ X Day Mastery Java EE "Soul chicken soup" of "development", which is a daunting "brick" book.<h4>1. The content is practical and targeted</h3>This book introduces Java EE The application example adopts the current enterprise popular development architecture and strictly abides by it Java EE Developing specifications, rather than cluttering together various technologies, is known as Java EE. With reference to the structure of this book, readers can feel the actual development of enterprises.<h4>2.The explanation of the framework source code level is in-depth and thorough</h3>
This book aims at Spring MVC,Spring,MyBatis The source code of the core part of the framework is explained, which can not only help readers really master the essence of the framework, but also let readers refer to the source code of excellent framework and quickly improve their technical skills. The source code interpretation method introduced in this book can also eliminate developers' fear of reading the framework source code, so that developers can calmly analyze problems when encountering technical problems and find the root cause of the problem from the framework source code level.<h4>3.Rich and detailed code for actual combat</h3>This book is a technical book for actual combat. We firmly believe that all knowledge points must be converted into code in order to finally become effective productivity. Therefore, this book provides corresponding executable example code for all knowledge points. The code not only has detailed comments, but also explains the examples in detail in combination with the theory, so as to really let the readers apply what they have learned.', 139.00, 2300);
insert into item_inf values
(3, 'Android handout', 'Java Language implementation, Android classic, stormzhang Liu Wangshu and Ke Junlin jointly recommended the ship and won the award CSDN Top ten original books with technical influence of the year', 'books/android.png', '<ul><li><insane Android The handout has been reprinted for 30 years since its launch+Times, circulation of nearly 200000 copies, and won many awards!</li><li>Open volume data shows "Crazy" Android Handout was ranked Android Top three books of the year</li><li><insane Android Lecture notes has been reviewed CSDN Top ten original books with technical influence of the year</li><li>Young opinion leaders StormZhang And multiple parts Android Niu Shu is jointly recommended by Liu Wangshu, Ke Junlin and Qijian</li><li>It has won the annual best selling book and long selling book award of electronic industry press for many times</li><li>It was awarded the annual "excellent publication" award by Gongxin publishing group</li></ul>', 138.00, 2300);
insert into item_inf values
(4, 'HTML 5/CSS3/JavaScript handout', 'HTML 5 And JavaScript Programming classic production, the necessary basis for front-end development', 'books/html.png', 'well-known IT Author Li Gang, teacher's masterpiece, the whole book is oriented to HTML5.1 The official version of the specification updates the relevant knowledge of multiple elements and drag and drop specifications, adds plug-in subtitles, dotted line mode and other contents, and focuses on the relevant features of the new mobile terminal<br>Detailed introduction of gradient background support, elastic box layout, mobile browser responsive layout, 3 D Transformation, etc CSS New features and major improvements', 99.00, 2300);
insert into item_inf values
(5, 'Python handout', 'Zero Basics Python Programming practice, CSDN Popular money Python The course specifies a book to cover employment hotspots such as crawler, big data and concurrent programming, Python No more panic in job hunting', 'books/python.png', '<ul><li>CSDN Popular course "21 days customs clearance" Python"Designated books.</li><li>Jingdong Technology IT The flower exploration work in the new book list was selected as Jingdong technology in 2019 IT Best seller list</li><li>Low starting threshold, 8-year-old children Charlie Through personal experience, you can not only understand the information in the book Python Basic knowledge of grammar, and wrote its own small program.</li><li>Covering a wide range of knowledge, the knowledge system is complete and systematic, and there is no need to "face Baidu" programming.</li></ul>', 118.00, 2300);
insert into item_inf values
(6, 'Lightweight Java EE Enterprise application practice—— Struts 2+Spring+Hibernate5/JPA2 Integrated development', 'S2SH Classic book upgrade, full embrace Spring 5 Lightweight Web Develop new features; It has been published for more than ten years and has been tested by hundreds of thousands of readers;', 'books/javaee.png', '<h4>1. Value added of books</h3>DVD The CD contains 1000 minutes of ultra long video, rich code and other content.<br>Provide readers with supporting websites, wechat groups QQ Group. Plus 107 major enterprises Java EE Interview questions, covering Java Web,Struts 2,Hibernate,Spring,Spring MVC,Help open famous enterprises Java Development gate.<h4>2. Award-winning</h3>This book has won the title of "the whole industry of the year" awarded by China Book Publishing Industry Association YouXiu "Best selling variety" award, and has won the best-selling book award awarded by the electronic industry press for many times, with a total printing of 40+Times.', 139.00, 2300);

miaosha_ Table creation statement of item table:

-- Second kill commodity Watch
drop table if exists miaosha_item;
create table miaosha_item
(
  miaosha_id bigint primary key auto_increment comment 'Second kill commodity Watch',
  item_id bigint comment 'commodity ID',
  miaosha_price decimal(10,2) comment 'price spike',
  stock_count int comment 'Inventory quantity',
  start_date datetime comment 'Spike start time',
  end_date datetime comment 'Spike end time',
  foreign key(item_id) references item_inf(item_id)
) comment='Second kill commodity Watch';
insert into miaosha_item values (1, 1, 1.98, 8, adddate(curdate(), -1), adddate(curdate(), 3));
insert into miaosha_item values (2, 2, 2.98, 8, adddate(curdate(), -1), adddate(curdate(), 2));
insert into miaosha_item values (3, 3, 3.98, 8, adddate(curdate(), -3), adddate(curdate(), -1));
insert into miaosha_item values (4, 4, 4.98, 8, adddate(curdate(), 1), adddate(curdate(), 5));
insert into miaosha_item values (5, 5, 5.98, 8, adddate(curdate(), -1), adddate(curdate(), 2));
insert into miaosha_item values (6, 6, 6.98, 8, adddate(curdate(), -1), adddate(curdate(), 2));

order_inf TABLE statement:

-- Order form
drop table if exists order_inf;
create table order_inf
(
  order_id bigint primary key auto_increment,
  user_id bigint comment 'user ID',
  item_id bigint comment 'commodity ID',
  item_name varchar(255) comment 'Redundant trade names to avoid multi table joins',
  order_num int comment 'Quantity of goods purchased',
  order_price decimal(10,2) comment 'Purchase price',
  order_channel tinyint comment 'Channels: 1 PC, 2,Android, 3,iOS',
  order_status tinyint comment 'Order status,0 New unpaid, 1 Paid,2 Shipped, 3 Received goods, 4 Refunded ,5 Completed',
  create_date datetime comment 'Creation time of the order',
  pay_date datetime comment 'Payment time',
  foreign key(user_id) references user_inf(user_id),
  foreign key(item_id) references item_inf(item_id)
)  comment='Order form';

miaosha_order table statement:

-- Second kill order form
drop table if exists miaosha_order;
create table miaosha_order
(
  miaosha_order_id bigint primary key auto_increment,
  user_id bigint comment 'user ID',
  order_id bigint comment 'order ID',
  item_id bigint comment 'commodity ID',
  unique key(user_id, item_id),
  foreign key(user_id) references user_inf(user_id),
  foreign key(order_id) references order_inf(order_id),
  foreign key(item_id) references item_inf(item_id)
) comment='Second kill order form';

This SQL statement is for Miaosha_ User of order table_ ID and item_ The combination of ID two columns defines a unique constraint, which limits the user from repeatedly killing the same commodity.

When the application starts, there is no order information in the system, so order_inf table and Miaosha_ There is no data in the order table.

2. Create domain object class

The system uses MyBatis to operate the database, but MyBatis is not a real ORM framework, but a result set mapping framework. Therefore, the domain objects required by the system are only some simple data classes.

User class:

import java.util.Date;
public class User
{
	private Long id;
	private String nickname;
	private String password;
	private String salt;
	private String head;
	private Date registerDate;
	private Date lastLoginDate;
	private Integer loginCount;

	public Long getId()
	{
		return id;
	}

	public void setId(Long id)
	{
		this.id = id;
	}

	public String getNickname()
	{
		return nickname;
	}

	public void setNickname(String nickname)
	{
		this.nickname = nickname;
	}

	public String getPassword()
	{
		return password;
	}

	public void setPassword(String password)
	{
		this.password = password;
	}

	public String getSalt()
	{
		return salt;
	}

	public void setSalt(String salt)
	{
		this.salt = salt;
	}

	public String getHead()
	{
		return head;
	}

	public void setHead(String head)
	{
		this.head = head;
	}

	public Date getRegisterDate()
	{
		return registerDate;
	}

	public void setRegisterDate(Date registerDate)
	{
		this.registerDate = registerDate;
	}

	public Date getLastLoginDate()
	{
		return lastLoginDate;
	}

	public void setLastLoginDate(Date lastLoginDate)
	{
		this.lastLoginDate = lastLoginDate;
	}

	public Integer getLoginCount()
	{
		return loginCount;
	}

	public void setLoginCount(Integer loginCount)
	{
		this.loginCount = loginCount;
	}

	@Override
	public String toString()
	{
		return "User{" +
				"id=" + id +
				", nickname='" + nickname + '\'' +
				", password='" + password + '\'' +
				", salt='" + salt + '\'' +
				", head='" + head + '\'' +
				", registerDate=" + registerDate +
				", lastLoginDate=" + lastLoginDate +
				", loginCount=" + loginCount +
				'}';
	}
}

Code of Item class:

public class Item
{
	private Long id;
	private String itemName;
	private String title;
	private String itemImg;
	private String itemDetail;
	private Double itemPrice;
	private Integer stockNum;

	public Long getId()
	{
		return id;
	}

	public void setId(Long id)
	{
		this.id = id;
	}

	public String getItemName()
	{
		return itemName;
	}

	public void setItemName(String itemName)
	{
		this.itemName = itemName;
	}

	public String getTitle()
	{
		return title;
	}

	public void setTitle(String title)
	{
		this.title = title;
	}

	public String getItemImg()
	{
		return itemImg;
	}

	public void setItemImg(String itemImg)
	{
		this.itemImg = itemImg;
	}

	public String getItemDetail()
	{
		return itemDetail;
	}

	public void setItemDetail(String itemDetail)
	{
		this.itemDetail = itemDetail;
	}

	public Double getItemPrice()
	{
		return itemPrice;
	}

	public void setItemPrice(Double itemPrice)
	{
		this.itemPrice = itemPrice;
	}

	public Integer getStockNum()
	{
		return stockNum;
	}

	public void setStockNum(Integer stockNum)
	{
		this.stockNum = stockNum;
	}

	@Override
	public String toString()
	{
		return "Item{" +
				"id=" + id +
				", itemName='" + itemName + '\'' +
				", title='" + title + '\'' +
				", itemImg='" + itemImg + '\'' +
				", itemDetail='" + itemDetail + '\'' +
				", itemPrice=" + itemPrice +
				", stockNum=" + stockNum +
				'}';
	}
}

MiaoshaItem inherits the Item class and adds some instance variables.

import java.util.Date;

public class MiaoshaItem extends Item
{
	private Long id;
	private Long itemId;
	private double miaoshaPrice;
	private Integer stockCount;
	private Date startDate;
	private Date endDate;

	public Long getId()
	{
		return id;
	}

	public void setId(Long id)
	{
		this.id = id;
	}

	public Long getItemId()
	{
		return itemId;
	}

	public void setItemId(Long itemId)
	{
		this.itemId = itemId;
	}

	public double getMiaoshaPrice()
	{
		return miaoshaPrice;
	}

	public void setMiaoshaPrice(double miaoshaPrice)
	{
		this.miaoshaPrice = miaoshaPrice;
	}

	public Integer getStockCount()
	{
		return stockCount;
	}

	public void setStockCount(Integer stockCount)
	{
		this.stockCount = stockCount;
	}

	public Date getStartDate()
	{
		return startDate;
	}

	public void setStartDate(Date startDate)
	{
		this.startDate = startDate;
	}

	public Date getEndDate()
	{
		return endDate;
	}

	public void setEndDate(Date endDate)
	{
		this.endDate = endDate;
	}

	@Override
	public String toString()
	{
		return "MiaoshaItem{" +
				"id=" + id +
				", itemId=" + itemId +
				", miaoshaPrice=" + miaoshaPrice +
				", stockCount=" + stockCount +
				", startDate=" + startDate +
				", endDate=" + endDate +
				'}';
	}
}

Order class:

import java.util.Date;

public class Order
{
	private Long id;
	private Long userId;
	private Long itemId;
	private String itemName;
	private Integer orderNum;
	private Double orderPrice;
	private Integer orderChannel;
	private Integer status;
	private Date createDate;
	private Date payDate;

	public Long getId()
	{
		return id;
	}

	public void setId(Long id)
	{
		this.id = id;
	}

	public Long getUserId()
	{
		return userId;
	}

	public void setUserId(Long userId)
	{
		this.userId = userId;
	}

	public Long getItemId()
	{
		return itemId;
	}

	public void setItemId(Long itemId)
	{
		this.itemId = itemId;
	}

	public String getItemName()
	{
		return itemName;
	}

	public void setItemName(String itemName)
	{
		this.itemName = itemName;
	}

	public Integer getOrderNum()
	{
		return orderNum;
	}

	public void setOrderNum(Integer orderNum)
	{
		this.orderNum = orderNum;
	}

	public Double getOrderPrice()
	{
		return orderPrice;
	}

	public void setOrderPrice(Double orderPrice)
	{
		this.orderPrice = orderPrice;
	}

	public Integer getOrderChannel()
	{
		return orderChannel;
	}

	public void setOrderChannel(Integer orderChannel)
	{
		this.orderChannel = orderChannel;
	}

	public Integer getStatus()
	{
		return status;
	}

	public void setStatus(Integer status)
	{
		this.status = status;
	}

	public Date getCreateDate()
	{
		return createDate;
	}

	public void setCreateDate(Date createDate)
	{
		this.createDate = createDate;
	}

	public Date getPayDate()
	{
		return payDate;
	}

	public void setPayDate(Date payDate)
	{
		this.payDate = payDate;
	}

	@Override
	public String toString()
	{
		return "Order{" +
				"id=" + id +
				", userId=" + userId +
				", itemId=" + itemId +
				", itemName='" + itemName + '\'' +
				", orderNum=" + orderNum +
				", orderPrice=" + orderPrice +
				", orderChannel=" + orderChannel +
				", status=" + status +
				", createDate=" + createDate +
				", payDate=" + payDate +
				'}';
	}
}

MiaoshaOrder class:

public class MiaoshaOrder
{
	private Long id;
	private Long userId;
	private Long orderId;
	private Long itemId;

	public Long getId()
	{
		return id;
	}

	public void setId(Long id)
	{
		this.id = id;
	}

	public Long getUserId()
	{
		return userId;
	}

	public void setUserId(Long userId)
	{
		this.userId = userId;
	}

	public Long getOrderId()
	{
		return orderId;
	}

	public void setOrderId(Long orderId)
	{
		this.orderId = orderId;
	}

	public Long getItemId()
	{
		return itemId;
	}

	public void setItemId(Long itemId)
	{
		this.itemId = itemId;
	}

	@Override
	public String toString()
	{
		return "MiaoshaOrder{" +
				"id=" + id +
				", userId=" + userId +
				", orderId=" + orderId +
				", itemId=" + itemId +
				'}';
	}
}

4, Implement Mapper(Dao layer)

The main advantage of MyBatis is that Mapper components can be used as DAO components. Developers only need to simply define Mapper interfaces and provide corresponding SQL statements for methods in Mapper interfaces through XML files, so that Mapper components can be developed.

Mapper component is used as DAO component, and mapper component is used to encapsulate database operation again, which is also a common DAO mode in Java EE applications.
When DAO mode is used, it not only reflects the facade mode that business logic components encapsulate Mapper components, but also separates the functions of business logic components and Mapper components:

  • The business logic component is responsible for the change of business logic
  • Mapper components are responsible for changes in Persistence technology
  • It is the application of bridge mode

When DAO mode is introduced, each Mapper component contains the access logic of the database. Each Mapper component can complete basic CRUD and other operations on a database table.

Dao mode is a development mode more in line with software engineering. The reasons for using DAO mode are as follows:

  • DAO mode abstracts the data access mode. The business logic component does not care about the underlying database access details, but only focuses on the implementation of business logic. The business logic component is only responsible for the change of business functions.
  • DAO centralizes data access in an independent layer, and all data access is completed by DAO components. This layer of independent DAO separates the implementation of data access from other business logic, making the system more maintainable.
  • DAO also helps to provide system portability. The independent DAO layer enables the system to easily switch between different databases, and the underlying database implementation is transparent to business logic components. Database migration only affects the DAO layer, and the switching between different databases will not affect the business logic components, which improves the reusability of the system.

1. Implement Mapper component

Mapper component provides basic CRUD operations for each persistent object, and mapper interface is responsible for declaring various crud methods that should be included in the component.
The methods in the MyBatis Mapper component are not automatically provided by the framework, but must be defined by the developer and provided with corresponding SQL statements. Therefore, the methods in the Mapper component may increase with the needs of business logic.

UserMaper interface definition:

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.crazyit.app.domain.User;

@Mapper
public interface UserMapper
{
	// According to user_id query user_ Record of inf table
	@Select("select user_id as id, nickname, password, salt, head, " +
			"register_date as registerDate, last_login_date as lastLoginDate, " +
			"login_count as loginCount from user_inf where user_id = #{id}")
	User findById(long id);
	// Update user_ Record of inf table
	@Update("update user_inf set last_login_date = #{lastLoginDate}" +
			", login_count=#{loginCount} where user_id = #{id}")
	void update(User user);
}

UserMapper provides two business methods as needed:

  • One method is based on user_id query user_ Records in inf table
  • One way is to update the user_ Records in inf table

MiaoshaItemMapper interface is defined as follows:

import org.apache.ibatis.annotations.*;
import org.crazyit.app.domain.MiaoshaItem;

import java.util.List;

@Mapper
public interface MiaoshaItemMapper
{
	// Query all second kill products
	@Select("select it.*,mi.stock_count, mi.start_date, mi.end_date, " +
			"mi.miaosha_price from miaosha_item mi left join item_inf " +
			"it on mi.item_id = it.item_id")
	@Results(id = "itemMapper", value = {
			@Result(property = "itemId", column = "item_id"),
			@Result(property = "itemName", column = "item_name"),
			@Result(property = "title", column = "title"),
			@Result(property = "itemImg", column = "item_img"),
			@Result(property = "itemDetail", column = "item_detail"),
			@Result(property = "itemPrice", column = "item_price"),
			@Result(property = "stockNum", column = "stock_num"),
			@Result(property = "miaoshaPrice", column = "miaosha_price"),
			@Result(property = "stockCount", column = "stock_count"),
			@Result(property = "startDate", column = "start_date"),
			@Result(property = "endDate", column = "end_date")
	})
	List<MiaoshaItem> findAll();
	// Query the second kill product according to the product ID
	@Select("select it.*,mi.stock_count, mi.start_date, mi.end_date, " +
			"mi.miaosha_price from miaosha_item mi left join item_inf it " +
			"on mi.item_id = it.item_id where it.item_id = #{itemId}")
	@ResultMap("itemMapper")
	MiaoshaItem findById(@Param("itemId") long itemId);
	// Update Miaosha_ Records in the item table
	@Update("update miaosha_item set stock_count = stock_count - 1" +
			" where item_id = #{itemId}")
	int reduceStock(MiaoshaItem miaoshaItem);
}

Three methods are provided:

  • Update inventory method
  • Query all second kill products
  • Query the specified second kill product according to the product ID

The OrderMapper interface is defined as follows:

import org.apache.ibatis.annotations.*;
import org.crazyit.app.domain.Order;

@Mapper
public interface OrderMapper
{
	// To order_ Insert new record in inf table
	@Insert("insert into order_inf(user_id, item_id, item_name, order_num, " +
			"order_price, order_channel, order_status, create_date) values" +
			"(#{userId}, #{itemId}, #{itemName}, #{orderNum}, #{orderPrice}, " +
			"#{orderChannel}, #{status}, #{createDate})")
	// Specifies to get the order_inf the self growing primary key value obtained when inserting records
	@Options(useGeneratedKeys = true, keyProperty = "id")
	long save(Order order);

	// Obtain the order according to the order ID and the ID of the ordering user
	@Select("select order_id as id, user_id as userId, item_id as itemId, " +
			"item_name as itemName, order_num as orderNum, order_price as " +
			"orderPrice, order_channel as orderChannel, order_status as " +
			"status, create_date as createDate, pay_date as payDate from " +
			"order_inf where order_id = #{param1} and user_id = #{param2}")
	Order findByIdAndOwnerId(long orderId, long userId);
}

The MiaoshaOrderMapper interface is defined as follows:

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.crazyit.app.domain.MiaoshaOrder;

@Mapper
public interface MiaoshaOrderMapper
{
	// Obtain the second kill order according to the user ID and commodity ID
	@Select("select miaosha_order_id as id, user_id as userId, order_id as " +
			"orderId, item_id as itemId from miaosha_order " +
			"where user_id=#{userId} and item_id=#{itemId}")
	MiaoshaOrder findByUserIdItemId(@Param("userId") long userId,
			@Param("itemId") long itemId);
	// Insert second kill order
	@Insert("insert into miaosha_order(user_id, item_id, order_id) values " +
			"(#{userId}, #{itemId}, #{orderId})")
	int save(MiaoshaOrder miaoshaOrder);
}

Mapper interface only needs to define the methods that mapper components should implement, and configure the corresponding SQL statements on the methods in mapper interface through annotations. These SQL statements are the key code to implement the methods in mapper components.

2. Deploy Mapper components

Just specify the necessary information to connect to the database in the application.properties file, and SpringBoot will automatically configure the data source, SqlSessionFactory and other basic components in the container. With these basic components, SpringBoot will automatically scan the @ Mapper annotations on the Mapper interface and deploy them as beans in the container.

# Database driven
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# Database URL
spring.datasource.url=jdbc:mysql://localhost:3306/miaosha_app?serverTimezone=UTC
# User name to connect to the database
spring.datasource.username=root
# Password to connect to the database
spring.datasource.password=32147

5, Implementation of distributed Session and user login

Seckill systems with high concurrency are distributed applications and need to use distributed Session. The user login and authority management of the system adopts distributed Session, which is implemented based on Redis.

1. Implement Redis components

The distributed Session of the system and the subsequent caching mechanism are implemented based on Redis.
In order for SpringBoot to provide automatic configuration for Redis integration, you need to add the following configuration in the application.properties file.

# -----------Redis related configuration-----------
spring.redis.host=localhost
spring.redis.port=6379
# Specify the DB0 database connected to Redis
spring.redis.database=0
# Connection password
spring.redis.password=32147
# The maximum number of active connections in the specified connection pool is 20
spring.redis.lettuce.pool.maxActive = 20
# The maximum number of free connections in the specified connection pool is 20
spring.redis.lettuce.pool.maxIdle=20
# The minimum number of free connections in the specified connection pool is 2
spring.redis.lettuce.pool.minIdle = 2

After the above configuration, SpringBoot will automatically configure RedisConnectionFactory and StringRedisTemplate for Redis in the container. Next, just inject the StringRedisTemplate component into other components.

2.Redis tools

The system has developed a tool class to encapsulate RedisTemplate. The encapsulated tool class can more easily operate the key value pairs in the system, including adding key value pairs, obtaining the corresponding value according to the key, deleting the specified key value pairs according to the key, judging whether the specified key exists, etc.

Fkretisutil class:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;

@Component
public class FkRedisUtil
{
	private final RedisTemplate<String, String> redisTemplate;
	private static final ObjectMapper objectMapper = new ObjectMapper();

	public FkRedisUtil(RedisTemplate<String, String> redisTemplate) {this.redisTemplate = redisTemplate;}

	// Get the corresponding value according to the key
	public <T> T get(KeyPrefix prefix, String key, Class<T> clazz)
	{
		// The actual key consists of prefix and key
		String realKey = prefix.getPrefix() + key;
		// Get the corresponding value according to the key
		String str = redisTemplate.opsForValue().get(realKey);
		try
		{
			// Restores the read string to a T object
			return stringToBean(str, clazz);
		}
		catch (JsonProcessingException e)
		{
			e.printStackTrace();
		}
		return null;
	}

	// Add key value pair
	public <T> Boolean set(KeyPrefix prefix, String key, T value)
	{
		String str = null;
		try
		{
			// Serializes the T object as a string
			str = beanToString(value);
		}
		catch (JsonProcessingException e)
		{
			e.printStackTrace();
		}
		if (str == null || str.length() <= 0)
		{
			return false;
		}
		// The actual key consists of prefix and key, and prefix also determines the expiration time of the key
		String realKey = prefix.getPrefix() + key;
		// Get expiration time
		int seconds = prefix.expireSeconds();
		// expireSeconds is the expiration time, and seconds < = 0 means never expires
		if (seconds <= 0)
		{
			// Here, add a common key to Redis. value is the string
			// If the expiration time is not set, it will never expire
			redisTemplate.opsForValue().set(realKey, str);
		}
		else
		{
			// The last parameter sets the expiration time. The expiration event here is in seconds
			redisTemplate.opsForValue().set(realKey, str,
					Duration.ofSeconds(seconds));
		}
		return true;
	}

	// Judge whether the specified key exists
	public Boolean exists(KeyPrefix prefix, String key)
	{
		String realPrefix = prefix.getPrefix() + key;
		return redisTemplate.hasKey(realPrefix);
	}

	// Delete data according to key
	public Boolean delete(KeyPrefix prefix, String key)
	{
		String realPrefix = prefix.getPrefix() + key;
		// Delete the specified key and the corresponding data
		return redisTemplate.delete(realPrefix);
	}

	// Add one to the value of the specified key
	public Long incr(KeyPrefix prefix, String key)
	{
		String realPrefix = prefix.getPrefix() + key;
		return redisTemplate.opsForValue().increment(realPrefix);
	}

	// Subtract one from the value of the specified key
	public Long decr(KeyPrefix prefix, String key)
	{
		String realPrefix = prefix.getPrefix() + key;
		return redisTemplate.opsForValue().decrement(realPrefix);
	}

	// Convert object to JSON string
	public static <T> String beanToString(T value)
			throws JsonProcessingException
	{
		if (value == null)
		{
			return null;
		}
		Class<?> clazz = value.getClass();
		// If the object to be converted is an integer, convert it to a string by adding an empty string
		if (clazz == Integer.class || clazz == int.class)
		{
			return "" + value;
		}
		else if (Long.class == clazz || clazz == long.class)
		{
			return "" + value;
		}
		else if (clazz == String.class)
		{
			return (String) value;
		}
		else
		{
			// Use Jackson to convert objects to JSON strings
			return objectMapper.writeValueAsString(value);
		}
	}

	// Convert JSON strings to objects
	public static <T> T stringToBean(String str, Class<T> clazz)
			throws JsonProcessingException
	{
		if (str == null || str.length() <= 0 || clazz == null)
		{
			return null;
		}
		// If the target object type to be recovered is integer, call the corresponding valueOf method for conversion
		if (clazz == int.class || clazz == Integer.class)
		{
			return (T) Integer.valueOf(str);
		}
		else if (clazz == long.class || clazz == Long.class)
		{
			return (T) Long.valueOf(str);
		}
		else if (clazz == String.class)
		{
			return (T) str;
		}
		else
		{
			// Convert JSON strings to objects using Jackson
			return objectMapper.readValue(str, clazz);
		}
	}
}

private final RedisTemplate<String, String> redisTemplate;

  • An instance variable of RedisTemplate type is defined, and SpringBoot will automatically complete dependency injection.

public T get(KeyPrefix prefix, String key, Class clazz)

  • The method of obtaining value according to key. When using this method to obtain value according to key, the actually used key is composed of keyprefix parameter and key parameter. String realKey = prefix.getPrefix() + key; The actual key is equal to the prefix+key parameter of the keyprefix parameter.

3.KeyPrefix interface

KeyPrefix is a user-defined interface that defines the prefix and expiration time.
The interface code is as follows:

public interface KeyPrefix
{
	int expireSeconds();

	String getPrefix();
}

When a key value pair needs to be added to Redis later in the program, just pass in different KeyPrefix parameters to achieve two purposes at the same time:

  • Control the prefix part of the added key, so as to avoid duplication caused by keys added in different places
  • Controls the expiration time of the key value pair. The expireSeconds() method of the prefix parameter returns the expiration time

4.KeyPrefix implementation class

In order to provide implementation classes for KeyPrefix later, an abstract implementation class is provided for KeyPrefix, which will be used as the base class of other KeyPrefix classes

public abstract class AbstractPrefix implements KeyPrefix
{
	private final int expireSeconds;
	private final String prefix;

	public AbstractPrefix(String prefix)
	{
		// Less than 0 means never expire
		this(-1, prefix);
	}

	public AbstractPrefix(int expireSeconds, String prefix)
	{
		// Set expiration time
		this.expireSeconds = expireSeconds;
		this.prefix = prefix;
	}

	@Override
	public int expireSeconds()
	{
		return expireSeconds;
	}

	// getPrefix will return the form of "class name: prefix"
	@Override
	public String getPrefix()
	{
		String className = getClass().getSimpleName();
		return className + ":" + prefix;
	}
}

getPrefix() method: the return value of this method is in the form of "class name: prefix", which means that the actual dkey prefix is always composed of class name and prefix.

5. Implementation of distributed Session

  • The implementation idea of distributed Session is to first send the Session ID to the browser and let the browser save the Session ID in the form of Cookie.
  • Then the server uses Redis to save the Session information
  • When saving Session information in Redis, take the Session ID saved by the client as the key.
  • When the user accesses the system, the system will first obtain the Session ID by reading the Cookie, and then read the information of the distributed Session through the Session ID

The implementation process is as follows:

  1. Every time a user accesses the system, the system will try to read the Session ID through a Cookie. If a valid Session ID cannot be read, the system will generate a random UUID as the Session ID, write the Session ID to the browser in the form of a Cookie and save it to the browser
  2. When the system needs to add Session information, the program stores the Session information in Redis in the form of key value pairs, where key is composed of the corresponding KeyPrefix and UUID(Session ID) generated in step 1, and value is the Session information to be saved
  3. When the system needs to read Session information, the program always reads the Session ID from the Cookie, and then takes the corresponding value from Redis according to the Session ID

In order to implement the above process, first define a tool class that can be used to operate cookies.
\app\controller\CookieUtil.java

import org.crazyit.app.redis.UserKey;
import org.crazyit.app.util.UUIDUtil;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CookieUtil
{
	// Tool method that writes the SessionID to the browser as a Cookie
	public static void addSessionId(HttpServletResponse response, String token)
	{
		// Use Cookie to store the ID of distributed Session
		Cookie cookie = new Cookie(UserKey.COOKIE_NAME_TOKEN, token);
		cookie.setMaxAge(UserKey.token.expireSeconds());
		cookie.setPath("/");
		response.addCookie(cookie);
	}
	// Tool method to read the value of the specified Cookie
	public static String getCookieValue(HttpServletRequest request,
			String cookieName)
	{
		// Get all cookies
		Cookie[] cookies = request.getCookies();
		if (cookies == null || cookies.length <= 0)
		{
			return null;
		}
		// Traverse all cookies
		for (Cookie cookie : cookies)
		{
			// Find and return the value of the target Cookie
			if (cookie.getName().equals(cookieName))
			{
				return cookie.getValue();
			}
		}
		return null;
	}
	// The tool method reads the ID of the distributed Session through the Cookie, and creates it if it does not exist
	public static String getSessionId(HttpServletRequest request,
			HttpServletResponse response)
	{
		// Get distributed SessionID through Cookie
		String token = CookieUtil.getCookieValue(request,
				UserKey.COOKIE_NAME_TOKEN);
		// If the SessionID is null, it indicates that the system was accessed for the first time or the Cookie has expired
		if (token == null)
		{
			// Generates a random string that will be used as the distributed SessionID
			token = UUIDUtil.uuid();
			// Write the distribution SessionID to the browser as a Cookie
			addSessionId(response, token);
		}
		return token;
	}
}

The tool class defines the following three methods:

  • addSessionId(): this method writes the Session ID to the browser in the form of a Cookie, and the browser is responsible for saving the Session ID
  • Getcookie value(): this method is used to read the specified Cookie value
  • getSessionId(): this method further encapsulates getcookie value(), which is used to read the value of Session ID according to the specified Cookie. If the Cookie does not exist, create it.

6.UserController adds distributed sessions and reads distributed sessions

\app\controller\UserController.java

import org.apache.commons.lang3.StringUtils;
import org.crazyit.app.domain.User;
import org.crazyit.app.exception.MiaoshaException;
import org.crazyit.app.redis.FkRedisUtil;
import org.crazyit.app.redis.UserKey;
import org.crazyit.app.result.CodeMsg;
import org.crazyit.app.result.Result;
import org.crazyit.app.service.UserService;
import org.crazyit.app.util.MD5Util;
import org.crazyit.app.vo.LoginVo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;

@Controller
@RequestMapping("/user")
public class UserController
{
	private final UserService userService;
	private final FkRedisUtil fkRedisUtil;
	public UserController(UserService userService, FkRedisUtil fkRedisUtil)
	{
		this.userService = userService;
		this.fkRedisUtil = fkRedisUtil;
	}

	@GetMapping("/login")
	public String toLogin()
	{
		return "login";
	}

	@GetMapping(value = "/verifyCode")
	@ResponseBody
	public void getLoginVerifyCode(HttpServletRequest request,
			HttpServletResponse response) throws IOException
	{
		// Read distributed Session ID from Cookie
		String token = CookieUtil.getSessionId(request, response);
		// Create verification code picture
		BufferedImage image = userService.createVerifyCode(token);
		OutputStream out = response.getOutputStream();
		// Output verification code
		ImageIO.write(image, "JPEG", out);
		out.flush();
		out.close();
	}

	@PostMapping("/proLogin")
	@ResponseBody
	public Result<Boolean> proLogin(HttpServletRequest request,
			HttpServletResponse response, LoginVo loginVo)
	{
		// Get distributed SessionID through Cookie
		String token = CookieUtil.getCookieValue(request,
				UserKey.COOKIE_NAME_TOKEN);
		// If a Cookie representing the distributed SessionID exists
		if (token != null)
		{
			// If the verification codes entered do not match
			if (!userService.checkVerifyCode(token,
					loginVo.getVercode()))
			{
				return Result.error(CodeMsg.REQUEST_ILLEGAL);
			}
			// Read user information from distributed Session
			User user = getByToken(response, token);
			// Judge whether the information read from the Session matches the login information
			if (user != null && user.getId().toString().equals(
					loginVo.getMobile()) && MD5Util.passToDbPass(
					loginVo.getPassword(),
					user.getSalt()).equals(user.getPassword()))
			{
				return Result.success(true);  // ①
			}
		}
		try
		{
			// Process login and return qualified users
			User user = userService.login(loginVo);  // ②
			// Use distributed Session to save login user information
			addSession(response, token, user);
			return Result.success(true);
		}
		catch (MiaoshaException e)
		{
			return Result.error(e.getCodeMsg());
		}
	}

	// This method uses Redis cache to implement distributed Session
	// This method saves the Session information in the Redis cache, and the SessionID is written to the browser as a Cookie
	private void addSession(HttpServletResponse response, String token, User user)
	{
		// Save distributed Session information in Redis cache
		fkRedisUtil.set(UserKey.token, token, user);
		// Use Cookie to store the ID of distributed Session
		CookieUtil.addSessionId(response, token);
	}

	// This method is used to read the corresponding User according to the distributed SessionID
	public User getByToken(HttpServletResponse response, String token)
	{
		if (StringUtils.isEmpty(token))
		{
			return null;
		}
		// Read the corresponding User according to the distributed SessionID
		User user = fkRedisUtil.get(UserKey.token, token, User.class);
		// Extend the validity period to ensure that the validity period is always the last access time plus the Session expiration time
		if (user != null)
		{
			// Set the token in the cache again and generate a new cookie, so as to extend the validity period
			addSession(response, token, user);
		}
		return user;
	}

	@GetMapping("/info")
	@ResponseBody
	public Result<User> info(User user)
	{
		return Result.success(user);
	}
}

The addSession() method is used to add the User object to the distributed Session, and the getByToken() method is used to read the User object through the distributed Session ID.

fkRedisUtil.set(UserKey.token, token, user); When fkretisutil is used to read and write key value pairs, the userkey class is used. This class implements the previous KeyPrefix interface, so it specifies both the prefix of the added key and the effective time of the added key.

Source code of UserKey class:
\app\redis\UserKey.java

public class UserKey extends AbstractPrefix
{
	public static final String COOKIE_NAME_TOKEN = "token";
	public static final int TOKEN_EXPIRE = 1800;

	public UserKey(int expireSeconds, String prefix)
	{
		super(expireSeconds, prefix);
	}
	// Define the key used to save the distributed Session ID
	public static UserKey token = new UserKey(TOKEN_EXPIRE, "token");
	// 0 means never expire
	public static UserKey getById = new UserKey(0, "id");
	// key used to save verification code
	public static UserKey verifyCode = new UserKey(300, "vc");
}

The expiration time of the key represented by User.token is 1800 seconds. The key prefix represented by User.token is UserKey:token, where token is the second parameter passed in when creating the UserKey object.

Define an interceptor and add a distributed Session ID. the interceptor code is as follows:
\app\access\AccessInterceptor.java

import org.apache.commons.lang3.StringUtils;
import org.crazyit.app.controller.CookieUtil;
import org.crazyit.app.controller.UserController;
import org.crazyit.app.domain.User;
import org.crazyit.app.redis.AccessKey;
import org.crazyit.app.redis.FkRedisUtil;
import org.crazyit.app.redis.UserKey;
import org.crazyit.app.result.CodeMsg;
import org.crazyit.app.result.Result;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

@Component
public class AccessInterceptor implements HandlerInterceptor
{
	private final UserController userController;
	private final FkRedisUtil fkRedisUtil;
	public AccessInterceptor(UserController userController,
			FkRedisUtil fkRedisUtil)
	{
		this.userController = userController;
		this.fkRedisUtil = fkRedisUtil;
	}

	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception
	{
		// Gets or creates the ID of the distributed Session
		CookieUtil.getSessionId(request, response);
		User user = getUser(request, response);
		// Store the read User information into the ThreadLocal container of UserContext
		UserContext.setUser(user);
		if (handler instanceof HandlerMethod)
		{
			HandlerMethod hm = (HandlerMethod) handler;
			// Get the @ AccessLimit annotation on the called method
			AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
			// If there is no @ AccessLimit annotation, it directly returns true (release)
			if (accessLimit == null)
			{
				return true;
			}
			int seconds = accessLimit.seconds();
			int maxCount = accessLimit.maxCount();
			boolean needLogin = accessLimit.needLogin();
			String key = request.getRequestURI();
			// If needLogin is true, it indicates that login is required to call this method
			if (needLogin)
			{
				// If the user is null, it indicates that the user is also logged in, and the call is directly rejected
				if (user == null)
				{
					render(response, CodeMsg.SESSION_ERROR);
					return false;
				}
			}
			// If the properties seconds and maxCount are set,
			// Indicates that the specified method can only be called several times within the specified time
			if (seconds > 0 && maxCount > 0)
			{
				key += "_" + user.getId();
				AccessKey ak = AccessKey.withExpire(seconds);
				// Take ak as the prefix and add the user's mobile phone number as the real key to obtain the number of visits
				Integer count = fkRedisUtil.get(ak, key, Integer.class);
				// If count is null, it indicates that it has not been accessed before
				if (count == null)
				{
					fkRedisUtil.set(ak, key, 1);
				}
				// If the number of visits has not reached the maximum, you can continue to visit
				else if (count < maxCount)
				{
					// Number of visits plus 1
					fkRedisUtil.incr(ak, key);
				}
				// If the number of visits reaches the limit
				else
				{
					// Generate error prompt
					render(response, CodeMsg.ACCESS_LIMIT_REACHED);
					return false;
				}
			}
		}
		return true;
	}
	// This method is used to generate an error response based on CodeMsg
	private void render(HttpServletResponse response,
			CodeMsg cm) throws IOException
	{
		response.setContentType("application/json;charset=UTF-8");
		OutputStream out = response.getOutputStream();
		// Wrap CodeMsg into a Result object and convert it into a string
		String str = FkRedisUtil.beanToString(Result.error(cm));
		// Output response string
		out.write(str.getBytes(StandardCharsets.UTF_8));
		out.flush();
		out.close();
	}

	private User getUser(HttpServletRequest request, HttpServletResponse response)
	{
		// Get the request parameter named token
		String paramToken = request.getParameter(UserKey.COOKIE_NAME_TOKEN);
		// Gets the value of the Cookie named token
		String cookieToken = CookieUtil.getCookieValue(request, UserKey.COOKIE_NAME_TOKEN);
		if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken))
		{
			return null;
		}
		// paramToken is preferred as the ID of the distributed Session
		String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
		// Obtain the Session object according to the distributed Session ID
		return userController.getByToken(response, token);
	}
}

AccessInterceptor implements the HandlerInterceptor interface. The preHandle() method in the implementation class intercepts all the controller's processing methods (as long as it is configured as interceptors), and the getSessionId() method called CookieUtil in preHandle() method is used to get or create distributed Session Id, which means that as long as users access the method of any controller in the system, The interceptor will write a Cookie to the visitor's browser to save the distributed session ID.

6, Implementation of user login

The page template used by the login function of the system is the login.html page. After the user submits the login request, the user name and password entered by the user are submitted to / user/proLogin. If the login is successful, the system will jump to / item/list. Otherwise, it will still stay on the login.html page and use the Layer library to display prompt information.
\app\controller\UserController.java

import org.apache.commons.lang3.StringUtils;
import org.crazyit.app.domain.User;
import org.crazyit.app.exception.MiaoshaException;
import org.crazyit.app.redis.FkRedisUtil;
import org.crazyit.app.redis.UserKey;
import org.crazyit.app.result.CodeMsg;
import org.crazyit.app.result.Result;
import org.crazyit.app.service.UserService;
import org.crazyit.app.util.MD5Util;
import org.crazyit.app.vo.LoginVo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;

@Controller
@RequestMapping("/user")
public class UserController
{
	private final UserService userService;
	private final FkRedisUtil fkRedisUtil;
	public UserController(UserService userService, FkRedisUtil fkRedisUtil)
	{
		this.userService = userService;
		this.fkRedisUtil = fkRedisUtil;
	}

	@GetMapping("/login")
	public String toLogin()
	{
		return "login";
	}

	@GetMapping(value = "/verifyCode")
	@ResponseBody
	public void getLoginVerifyCode(HttpServletRequest request,
			HttpServletResponse response) throws IOException
	{
		// Read distributed Session ID from Cookie
		String token = CookieUtil.getSessionId(request, response);
		// Create verification code picture
		BufferedImage image = userService.createVerifyCode(token);
		OutputStream out = response.getOutputStream();
		// Output verification code
		ImageIO.write(image, "JPEG", out);
		out.flush();
		out.close();
	}

	@PostMapping("/proLogin")
	@ResponseBody
	public Result<Boolean> proLogin(HttpServletRequest request,
			HttpServletResponse response, LoginVo loginVo)
	{
		// Get distributed SessionID through Cookie
		String token = CookieUtil.getCookieValue(request,
				UserKey.COOKIE_NAME_TOKEN);
		// If a Cookie representing the distributed SessionID exists
		if (token != null)
		{
			// If the verification codes entered do not match
			if (!userService.checkVerifyCode(token,
					loginVo.getVercode()))
			{
				return Result.error(CodeMsg.REQUEST_ILLEGAL);
			}
			// Read user information from distributed Session
			User user = getByToken(response, token);
			// Judge whether the information read from the Session matches the login information
			if (user != null && user.getId().toString().equals(
					loginVo.getMobile()) && MD5Util.passToDbPass(
					loginVo.getPassword(),
					user.getSalt()).equals(user.getPassword()))
			{
				return Result.success(true);  // ①
			}
		}
		try
		{
			// Process login and return qualified users
			User user = userService.login(loginVo);  // ②
			// Use distributed Session to save login user information
			addSession(response, token, user);
			return Result.success(true);
		}
		catch (MiaoshaException e)
		{
			return Result.error(e.getCodeMsg());
		}
	}

	// This method uses Redis cache to implement distributed Session
	// This method saves the Session information in the Redis cache, and the SessionID is written to the browser as a Cookie
	private void addSession(HttpServletResponse response, String token, User user)
	{
		// Save distributed Session information in Redis cache
		fkRedisUtil.set(UserKey.token, token, user);
		// Use Cookie to store the ID of distributed Session
		CookieUtil.addSessionId(response, token);
	}

	// This method is used to read the corresponding User according to the distributed SessionID
	public User getByToken(HttpServletResponse response, String token)
	{
		if (StringUtils.isEmpty(token))
		{
			return null;
		}
		// Read the corresponding User according to the distributed SessionID
		User user = fkRedisUtil.get(UserKey.token, token, User.class);
		// Extend the validity period to ensure that the validity period is always the last access time plus the Session expiration time
		if (user != null)
		{
			// Set the token in the cache again and generate a new cookie, so as to extend the validity period
			addSession(response, token, user);
		}
		return user;
	}

	@GetMapping("/info")
	@ResponseBody
	public Result<User> info(User user)
	{
		return Result.success(user);
	}
}

The following three URL addresses are mapped:

  • /user/login: used to enter the login page
  • /user/verifyCode: used to generate graphic verification code
  • /user/proLogin: used to handle user login

/The proLogin() method corresponding to user/proLogin first reads the Session ID from the client Cookie, and then reads the User information (Session information) from Redis according to the Session ID. If the User information read from Redis is the same as the logged in User information, it indicates that the User is logging in repeatedly, so the login success is returned directly.

Only when the user has not logged in before, the proLogin() method will call the login() method of UserService to process the user login.

import org.crazyit.app.dao.UserMapper;
import org.crazyit.app.domain.User;
import org.crazyit.app.exception.MiaoshaException;
import org.crazyit.app.redis.FkRedisUtil;
import org.crazyit.app.redis.UserKey;
import org.crazyit.app.result.CodeMsg;
import org.crazyit.app.util.MD5Util;
import org.crazyit.app.util.VercodeUtil;
import org.crazyit.app.vo.LoginVo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.awt.image.BufferedImage;
import java.util.Date;
import java.util.Random;

@Service
public class UserService
{
	private final UserMapper userMapper;
	private final FkRedisUtil fkRedisUtil;
	public UserService(UserMapper userMapper, FkRedisUtil fkRedisUtil)
	{
		this.userMapper = userMapper;
		this.fkRedisUtil = fkRedisUtil;
	}
	// Create drawing verification code
	public BufferedImage createVerifyCode(String token)
	{
		if (token == null)
		{
			return null;
		}
		Random rdm = new Random();
		// Call generateVerifyCode of VercodeUtil to generate graphic verification code
		String verifyCode = VercodeUtil.generateVerifyCode(rdm);
		// Calculate the value of the graphic verification code
		int rnd = VercodeUtil.calc(verifyCode);
		// Save the value of the verification code in Redis
		fkRedisUtil.set(UserKey.verifyCode, token, rnd);
		// Returns the generated picture
		return VercodeUtil.createVerifyImage(verifyCode, rdm);
	}
	// Check whether the graphic verification code is correct
	public boolean checkVerifyCode(String token, int verifyCode)
	{
		if (token == null)
		{
			return false;
		}
		// Read the verification code saved by the server from Redis
		Integer codeOld = fkRedisUtil.get(UserKey.verifyCode,
				token, Integer.class);
		// Returns false if codeOld is empty or if codeOld is different from verifyCode
		if (codeOld == null || codeOld - verifyCode != 0)
		{
			return false;
		}
		// Clear the graphic verification code saved by the server
		fkRedisUtil.delete(UserKey.verifyCode, token);
		return true;
	}
	// Method of handling user login
	@Transactional
	public User login(LoginVo loginVo)
	{
		if (loginVo == null)
		{
			throw new MiaoshaException(CodeMsg.SERVER_ERROR);
		}
		String mobile = loginVo.getMobile();
		// Obtain the corresponding user according to the mobile phone number
		User user = getById(Long.parseLong(mobile));  // ①
		// If user is null, the user does not exist
		if (user == null)
		{
			throw new MiaoshaException(CodeMsg.MOBILE_NOT_EXIST);
		}
		// Get the password saved in the database
		String dbPass = user.getPassword();
		// Calculate the password after salt encryption
		String calcPass = MD5Util.passToDbPass(loginVo.getPassword(),
				user.getSalt());
		// If the password encrypted with salt is not equal to the password saved in the database, the login fails
		if (!calcPass.equals(dbPass))
		{
			throw new MiaoshaException(CodeMsg.PASSWORD_ERROR);
		}
		// Increase login times
		user.setLoginCount(user.getLoginCount() + 1);
		// Update last logon time
		user.setLastLoginDate(new Date());
		// Update user information
		userMapper.update(user);
		return user;
	}

	private User getById(long id)
	{
		// First read the user ID from the Redis cache
		User user = fkRedisUtil.get(UserKey.getById,
				"" + id, User.class);
		if (user != null)
		{
			return user;
		}
		// If no user is read in Redis cache, read the user ID from the database
		user = userMapper.findById(id);
		if (user != null)
		{
			// Save the read user into Redis cache
			fkRedisUtil.set(UserKey.getById, "" + id, user);
		}
		return user;
	}
}

The /user/verifyCode corresponding getLoginVerifyCode() method also reads Session ID from the client Cookie first, then calls UserService createVerifyCode() method to generate the graphic verification code, and outputs the graphic verification code to the client.

login() method calls getById() method to get users according to the mobile phone number. After obtaining the user, the user input password is added with salt to encrypt, then the password is encrypted with salt and compared with the password in the database. If the two passwords are identical, we can think that the record is successful.

MD5Util tool class:

import org.apache.commons.codec.digest.DigestUtils;

public class MD5Util
{
	public static String md5(String src)
	{
		return DigestUtils.md5Hex(src);
	}

	public static String passToDbPass(String formPass, String randSalt)
	{
		String str = "" + randSalt.charAt(0) + randSalt.charAt(2)
				+ formPass + randSalt.charAt(5) + randSalt.charAt(4);
		return md5(str);
	}

	public static void main(String[] args)
	{
		// Salt encrypted password
		System.out.println(passToDbPass("123456", "0p9o8i"));
	}
}

The passToDbPass() method is used to encrypt and salt the specified password.
The logic of the getById() method is relatively simple. This method first attempts to read the user ID (mobile phone number) from the Redis cache. If there is no corresponding user in the Redis cache, try to read the user from the underlying database. If the corresponding user is read from the underlying database, store the user in the Redis cache.

7, Graphic verification code

UserService calls the generateVerifyCode() method of VercodeUtil to generate a random graphic verification code, also calls the calc() method to calculate the value of the verification code, and calls the createVefifyImage() method to generate a verification code picture.

VercodeUtil is a tool class for generating verification codes. It uses expression verification codes and generates an expression on the verification code picture. Users must fill in the value of the expression to pass the verification.

import org.crazyit.app.domain.User;
import org.crazyit.app.redis.MiaoshaKey;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

public class VercodeUtil
{
	private static final char[] ops = new char[]{'+', '-', '*'};
	// Expression for generating graphic verification code
	public static String generateVerifyCode(Random rdm)
	{
		// Generate four random integers
		int num1 = rdm.nextInt(10) + 1;
		int num2 = rdm.nextInt(10) + 1;
		int num3 = rdm.nextInt(10) + 1;
		int num4 = rdm.nextInt(10) + 1;
		var opsLen = ops.length;
		// Generate three random operators
		char op1 = ops[rdm.nextInt(opsLen)];
		char op2 = ops[rdm.nextInt(opsLen)];
		char op3 = ops[rdm.nextInt(opsLen)];
		// Concatenate integers and operators into expressions
		return "" + num1 + op1 + num2 + op2 + num3 + op3 + num4;
	}
	// Generate a verification code picture according to the graphic verification code expression
	public static BufferedImage createVerifyImage(String verifyCode, Random rdm)
	{
		var width = 120;
		var height = 32;
		// Create drawing
		var image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		Graphics g = image.getGraphics();
		// Set background color
		g.setColor(new Color(0xDCDCDC));
		g.fillRect(0, 0, width, height);
		// bound box 
		g.setColor(Color.black);
		g.drawRect(0, 0, width - 1, height - 1);
		// Generate some interference ellipses
		for (int i = 0; i < 50; i++)
		{
			int x = rdm.nextInt(width);
			int y = rdm.nextInt(height);
			g.drawOval(x, y, 0, 0);
		}
		// Set color
		g.setColor(new Color(0, 100, 0));
		// Set font
		g.setFont(new Font("Candara", Font.BOLD, 24));
		// Drawing graphic verification code
		g.drawString(verifyCode, 8, 24);
		g.dispose();
		// Return picture
		return image;
	}
	public static int calc(String exp)
	{
		try
		{
			// Gets the script engine that evaluates the value of an expression
			ScriptEngineManager manager = new ScriptEngineManager();
			ScriptEngine engine = manager.getEngineByName("JavaScript");
			// Evaluate the value of the expression
			return (Integer) engine.eval(exp);
		}
		catch (Exception e)
		{
			e.printStackTrace();
			return 0;
		}
	}
}

generateVerifyCode() is used to generate a verification code expression. It is divided into four random integers and then three random operators. It is spliced to form a verification code expression.
The createVerifyImage() method uses AWT Graphics to draw images.
The calc() method of the tool class uses the eval() method of ScriptEngine to calculate the value of the expression. Here, the built-in JavaScript script engine of JDK is used. It is very convenient to calculate the value of the expression by using JavaScript requests.

8, Implementation of login page

The login page of the system will use jQuery to send a request to perform asynchronous login, and use the Layer library to display the login results.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
	<meta charset="UTF-8">
	<title>Sign in</title>
	<!-- jQuery -->
	<script type="text/javascript" th:src="@{/jquery/jquery.min.js}"></script>
	<!-- BootStrap -->
	<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
	<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
	<!-- jQuery-Validation -->
	<script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}">
	</script>
	<script type="text/javascript" th:src=
			"@{/jquery-validation/localization/messages_zh.min.js}"></script>
	<!-- Layer -->
	<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
	<!-- Custom common.js -->
	<script type="text/javascript" th:src="@{/js/common.js}"></script>
	<script>
		function login() {
			// Perform input verification
			$("#loginForm").validate({
				// If the input verification passes, execute the submitLogin() function
				submitHandler: function () {
					submitLogin();
				}
			});
		}

		function submitLogin() {
			g_showLoading();
			// Send POST request
			$.post("/user/proLogin", {
				mobile: $("#mobile").val(),
				vercode: $("#vercode").val(),
				password: $("#password").val()
			}, function (data) {
				layer.closeAll();
				// code 0 means success
				if (data.code == 0) {
					// Display a prompt box similar to Android Toast on the screen
					layer.msg("Login succeeded");
					// Jump after successful login
					window.location.href = "/item/list";
				} else {
					// Display error message
					layer.msg(data.msg);
				}
			});
		}

		$(function () {
			refreshVerifyCode();
		});

		// Defines the function to refresh the verification code
		function refreshVerifyCode() {
			$("#verifyCodeImg").attr("src",
				"/user/verifyCode?timestamp=" + new Date().getTime());
		}
	</script>
</head>
<body>
<div class="container">
	<img th:src="@{/imgs/logo.png}"
		 class="rounded mx-auto d-block" alt="logo"><h4>User login</h4>
	<form name="loginForm" id="loginForm" method="post">
		<div class="form-group row">
			<label for="mobile" class="col-sm-3 col-form-label">cell-phone number:</label>
			<div class="col-sm-9">
				<input type="text" id="mobile" name="mobile"
					   required="true" minlength="11" maxlength="11"
					   class="form-control" placeholder="Please enter your mobile phone number">
			</div>
		</div>
		<div class="form-group row">
			<label for="password" class="col-sm-3 col-form-label">password:</label>
			<div class="col-sm-9">
				<input type="password" id="password" name="password"
					   required="true" minlength="6" maxlength="16"
					   class="form-control" placeholder="Please input a password">
			</div>
		</div>
		<div class="form-group row">
			<label for="vercode" class="col-sm-3 col-form-label">Verification Code:</label>
			<div class="col-sm-7">
				<input type="text" id="vercode" name="vercode" class="form-control"
					   required="true" placeholder="Please enter the verification code"/>
			</div>
			<div class="col-sm-2">
				<img id="verifyCodeImg" width="80" height="32" alt="Verification Code"
					 th:src="@{/user/verifyCode}" onclick="refreshVerifyCode()"/>
			</div>
		</div>
		<div class="form-group row">
			<div class="col-sm-6 text-right">
				<button type="submit" class="btn btn-primary"
						onclick="login()">Sign in
				</button>
			</div>
			<div class="col-sm-6">
				<button type="reset" class="btn btn-danger"
						onclick="$('#Loginform '). Reset() "> Reset
				</button>
			</div>
		</div>
	</form>
</div>
</body>
</html>

9, Implementation of second kill commodity list and cache

/item/list is used to display the second kill product list. Because the second kill product list page needs to be accessed frequently, and the page does not need to provide different interfaces for different users, the system will cache the static content of the page.

1. Second kill product list

The ItemController controller defines the processing method for displaying the second kill product list

import org.apache.commons.lang3.StringUtils;
import org.crazyit.app.access.AccessLimit;
import org.crazyit.app.domain.MiaoshaItem;
import org.crazyit.app.domain.User;
import org.crazyit.app.redis.FkRedisUtil;
import org.crazyit.app.redis.ItemKey;
import org.crazyit.app.result.Result;
import org.crazyit.app.service.MiaoshaService;
import org.crazyit.app.vo.ItemDetailVo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.thymeleaf.context.IWebContext;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;

@Controller
@RequestMapping("/item")
public class ItemController
{
	private final MiaoshaService miaoshaService;
	private final FkRedisUtil fkRedisUtil;
	// Defines the ThymeleafViewResolver used to parse the Thymeleaf page template
	private final ThymeleafViewResolver thymeleafViewResolver;
	public ItemController(MiaoshaService miaoshaService, FkRedisUtil fkRedisUtil,
			ThymeleafViewResolver thymeleafViewResolver)
	{
		this.miaoshaService = miaoshaService;
		this.fkRedisUtil = fkRedisUtil;
		this.thymeleafViewResolver = thymeleafViewResolver;
	}

	@GetMapping("/list")
	@ResponseBody
	@AccessLimit // Restrict that this method must be logged in to access
	public String list(HttpServletRequest request,
			HttpServletResponse response, User user)
	{
		// Fetch data from Redis cache
		String html = fkRedisUtil.get(ItemKey.itemList, "", String.class);
		// If there are HTML pages in the cache, the HTML page is returned directly
		if (!StringUtils.isEmpty(html))
		{
			return html;
		}
		// If there is no HTML page in the cache, the query will be executed
		// Query seckill product list
		List<MiaoshaItem> itemList = miaoshaService.listMiaoshaItem(); // ①
		IWebContext ctx = new WebContext(request, response,
				request.getServletContext(), request.getLocale(),
				Map.of("user", user, "itemList", itemList));
		// Render static HTML content
		html = thymeleafViewResolver.getTemplateEngine().process("item_list", ctx);
		// Cache static HTML content
		if (!StringUtils.isEmpty(html))
		{
			fkRedisUtil.set(ItemKey.itemList, "", html); // ②
		}
		return html;
	}

	@GetMapping(value = "/detail/{itemId}")
	@ResponseBody
	@AccessLimit // Restrict that this method must be logged in to access
	public Result<ItemDetailVo> detail(User user,
			@PathVariable("itemId") long itemId)
	{
		MiaoshaItem item = miaoshaService.getMiaoshaItemById(itemId);
		// Get the start time of second kill
		long startAt = item.getStartDate().getTime();
		// Get the end time of the second kill
		long endAt = item.getEndDate().getTime();
		long now = System.currentTimeMillis();
		// A variable that defines how long before the start of the second kill
		int remainSeconds;
		if (now < startAt)
		{
			// The second kill hasn't started yet
			remainSeconds = (int) ((startAt - now) / 1000);
		}
		else if (now > endAt)
		{
			// The second kill is over
			remainSeconds = -1;
		}
		else
		{
			// Second kill in progress
			remainSeconds = 0;
		}
		// A variable that defines how long the second kill ends
		var leftSeconds = (int) ((endAt - now ) / 1000);
		// Create ItemDetailVo to encapsulate the details of seckill products
		ItemDetailVo itemDetailVo = new ItemDetailVo();
		itemDetailVo.setMiaoshaItem(item);
		itemDetailVo.setUser(user);
		itemDetailVo.setRemainSeconds(remainSeconds);
		itemDetailVo.setLeftSeconds(leftSeconds);
		return Result.success(itemDetailVo);
	}
}

fkRedisUtil.get is used to read the rendered HTML static content from the Redis cache. Only when the static content does not exist, the controller will call the listMiaoshaItem() method of MiaoshaService to obtain all second kill products.

After fkretisutil.set obtains the list of all second kill products through the listMiaoshaItem() method, use ThymeleafViewResolver to perform page rendering, generate static HTML page content, and store the static HTML page content in Redis cache.

In this way, when multiple users access the list page in high concurrency, only the user who accesses "/ item/list" for the first time really needs to call the method of the Service component to query the underlying database, and other users will directly use the HTML page content in Redis cache.
\app\redis\ItemKey.java

public class ItemKey extends AbstractPrefix
{
	public ItemKey(int expireSeconds, String prefix)
	{
		super(expireSeconds, prefix);
	}
	// Cache the key prefix of the second kill product list page
	public static ItemKey itemList = new ItemKey(120, "list");
	// Cache the key prefix of the second kill inventory
	public static ItemKey miaoshaItemStock = new ItemKey(0, "stock");
}

ItemKey determines that the time to cache the second kill product list page is 120 seconds, that is, 2 minutes, which means that no matter how many concurrent requests there are within 2 minutes, the list() processing method only needs to call the Service component once, so that it can calmly face high concurrent requests.

The method to obtain the second kill list in MiaoshaService is as follows:

import java.awt.image.BufferedImage;
import java.util.Date;
import java.util.List;
import java.util.Random;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import org.crazyit.app.dao.MiaoshaItemMapper;
import org.crazyit.app.dao.MiaoshaOrderMapper;
import org.crazyit.app.dao.OrderMapper;
import org.crazyit.app.domain.MiaoshaItem;
import org.crazyit.app.domain.MiaoshaOrder;
import org.crazyit.app.domain.Order;
import org.crazyit.app.domain.User;
import org.crazyit.app.redis.MiaoshaKey;
import org.crazyit.app.redis.FkRedisUtil;
import org.crazyit.app.redis.OrderKey;
import org.crazyit.app.util.MD5Util;
import org.crazyit.app.util.UUIDUtil;
import org.crazyit.app.util.VercodeUtil;

@Service
public class MiaoshaService
{
	private final FkRedisUtil fkRedisUtil;
	private final MiaoshaItemMapper miaoshaItemMapper;
	private final OrderMapper orderMapper;
	private final MiaoshaOrderMapper miaoshaOrderMapper;

	public MiaoshaService(FkRedisUtil fkRedisUtil,
			MiaoshaItemMapper miaoshaItemMapper,
			OrderMapper orderMapper,
			MiaoshaOrderMapper miaoshaOrderMapper)
	{
		this.fkRedisUtil = fkRedisUtil;
		this.miaoshaItemMapper = miaoshaItemMapper;
		this.orderMapper = orderMapper;
		this.miaoshaOrderMapper = miaoshaOrderMapper;
	}

	// List the methods of all second kill products
	public List<MiaoshaItem> listMiaoshaItem()
	{
		return miaoshaItemMapper.findAll();
	}

	// Method for obtaining second kill commodity according to commodity ID
	public MiaoshaItem getMiaoshaItemById(long itemId)
	{
		return miaoshaItemMapper.findById(itemId);
	}

	// Method of executing second kill
	@Transactional
	public Order miaosha(User user, MiaoshaItem item)
	{
		// Reduce the inventory of second kill goods by 1
		boolean success = reduceStock(item);
		if (success)
		{
			// Create ordinary orders and second kill orders
			return createOrder(user, item);
		}
		else
		{
			// If the second kill fails, set the second kill status of the commodity to ended
			fkRedisUtil.set(MiaoshaKey.isItemOver,
					"" + item.getId(), true);
			return null;
		}
	}

	// Reduce the inventory of second kill goods by 1
	public boolean reduceStock(MiaoshaItem miaoshaItem)
	{
		int ret = miaoshaItemMapper.reduceStock(miaoshaItem);
		return ret > 0;
	}

	// The second kill order id is returned according to the user id and item id,
	// If the second kill is not successful, return - 1 when the second kill is over, and return 0 when the second kill is not over
	public long getMiaoshaResult(Long userId, long itemId)
	{
		// Obtain the second kill order according to the user ID and commodity ID
		MiaoshaOrder order = getMiaoshaOrderByUserIdAndItemId(userId, itemId);
		// If the second kill order is not null, the order ID is returned
		if (order != null)
		{
			return order.getOrderId();
		}
		else
		{
			// Obtain the second kill status of the commodity according to the article ID
			boolean isOver = fkRedisUtil
					.exists(MiaoshaKey.isItemOver, "" + itemId);
			// If the second kill is over, return - 1
			if (isOver)
			{
				return -1;
			}
			// Otherwise, 0 is returned
			else
			{
				return 0;
			}
		}
	}

	// Judge whether the second kill address entered by the user is correct
	public boolean checkPath(User user, long itemId, String path)
	{
		if (user == null || path == null)
		{
			return false;
		}
		// Get UUID string of Redis cache
		String pathOld = fkRedisUtil.get(MiaoshaKey.miaoshaPath, ""
				+ user.getId() + "_" + itemId, String.class);
		// Compare the UUID string entered by the user with the UUID string cached by Redis
		return path.equals(pathOld);
	}

	// Method for generating second kill address
	public String createMiaoshaPath(User user, long itemId)
	{
		if (user == null || itemId <= 0)
		{
			return null;
		}
		// The user creates a UUID string and MD5 encrypts the UUID string
		String str = MD5Util.md5(UUIDUtil.uuid());
		// Store the dynamically generated seckill address in Redis
		fkRedisUtil.set(MiaoshaKey.miaoshaPath, ""
				+ user.getId() + "_" + itemId, str);
		return str;
	}

	// Generate second kill graphic verification code
	public BufferedImage createVerifyCode(User user, long itemId)
	{
		if (user == null || itemId <= 0)
		{
			return null;
		}
		Random rdm = new Random();
		String verifyCode = VercodeUtil.generateVerifyCode(rdm);
		int rnd = VercodeUtil.calc(verifyCode);
		// Save the value of the verification code in Redis
		fkRedisUtil.set(MiaoshaKey.miaoshaVerifyCode,
				user.getId() + "," + itemId, rnd);
		// Returns the generated picture
		return VercodeUtil.createVerifyImage(verifyCode, rdm);
	}

	// Check whether the second kill verification code entered by the user is correct
	public boolean checkVerifyCode(User user, long itemId, int verifyCode)
	{
		if (user == null || itemId <= 0)
		{
			return false;
		}
		// Get the verification code saved in Redis
		Integer codeOld = fkRedisUtil.get(MiaoshaKey.miaoshaVerifyCode,
			user.getId() + "," + itemId, Integer.class);
		// Compare the verification code entered by the user with the verification code saved in Redis
		if (codeOld == null || codeOld - verifyCode != 0)
		{
			return false;
		}
		// Delete the verification code saved in Redis
		fkRedisUtil.delete(MiaoshaKey.miaoshaVerifyCode,
			user.getId() + "," + itemId);
		return true;
	}

	// Obtain the second kill order according to the user ID and commodity ID
	public MiaoshaOrder getMiaoshaOrderByUserIdAndItemId(long userId, long itemId)
	{
		// Read order from Redis cache
		return fkRedisUtil.get(OrderKey.miaoshaOrderByUserIdAndItemId,
				"" + userId + "_" + itemId, MiaoshaOrder.class);
	}

	// Create ordinary orders and second kill orders
	@Transactional
	public Order createOrder(User user, MiaoshaItem item)
	{
		// Create normal order
		var order = new Order();
		// Set order information
		order.setUserId(user.getId());
		order.setCreateDate(new Date());
		order.setOrderNum(1);
		order.setItemId(item.getItemId());
		order.setItemName(item.getItemName());
		order.setOrderPrice(item.getMiaoshaPrice());
		order.setOrderChannel(1);
		// Set order status, 0 represents unpaid order
		order.setStatus(0);
		// Save normal order
		orderMapper.save(order);
		// Create second kill order
		var miaoshaOrder = new MiaoshaOrder();
		// Set second kill order information
		miaoshaOrder.setUserId(user.getId());
		miaoshaOrder.setItemId(item.getItemId());
		miaoshaOrder.setOrderId(order.getId());
		// Save second kill order
		miaoshaOrderMapper.save(miaoshaOrder);
		// Save the second kill order to Redis cache
		fkRedisUtil.set(OrderKey.miaoshaOrderByUserIdAndItemId,
				"" + user.getId() + "_" + item.getItemId(), miaoshaOrder);
		return order;
	}

	// Method of obtaining order according to order ID and user ID
	public Order getOrderByIdAndOwnerId(long orderId, long userId)
	{
		return orderMapper.findByIdAndOwnerId(orderId, userId);
	}
}

MiaoshaService component called the findAll() method of miaoshaItemMapper component to get all the second kill product lists.

2. User defined parameter parser

There is a User parameter in the list() method of ItemController. This method can only be called after the User logs in. Obviously, this parameter should be read from redis (distributed Session). All users need to use a custom User parameter parser to process this User parameter.
\app\config\UserArgumentResolver.java

import org.crazyit.app.access.UserContext;
import org.crazyit.app.domain.User;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver
{
	// If the method returns true, it indicates that the parameter is to be resolved
	@Override
	public boolean supportsParameter(MethodParameter methodParameter)
	{
		// Gets the parameter type to resolve
		Class<?> clazz = methodParameter.getParameterType();
		// The resolveArgument method below will be called to resolve the parameter only when the return value is true
		return clazz == User.class;
	}

	@Override
	public Object resolveArgument(MethodParameter methodParameter,
			ModelAndViewContainer modelAndViewContainer,
			NativeWebRequest nativeWebRequest,
			WebDataBinderFactory webDataBinderFactory)
	{
		// Take the return value of getUser() method of UserContext as the value of User parameter
		return UserContext.getUser();
	}
}

The parameter parser class implements the HandlerMethodArgumentResolver interface, and its resolveArgument() method will be responsible for parsing the parameters in the controller processing method. clazz == User.class; determines that the parameter parser will only parse the User parameter

The resolveArgument() method takes the return value of the getUser() method of UserContext as the User parameter value.

\app\access\UserContext.java

import org.crazyit.app.domain.User;

public class UserContext
{
	private static final ThreadLocal<User> userHolder = new ThreadLocal<>();

	public static void setUser(User user)
	{
		userHolder.set(user);
	}

	public static User getUser()
	{
		return userHolder.get();
	}
}

It can be seen from the code of UserContext class that UserContext only uses ThreadLocal container to save User information. ThreadLocal will ensure that each thread holds a User copy, while the resolveArgument() method of UserArgumentResolver only obtains the User object from ThreadLocal container, and the AccessInterceptor interceptor is responsible for putting the User object into the ThreadLocal container.

import org.apache.commons.lang3.StringUtils;
import org.crazyit.app.controller.CookieUtil;
import org.crazyit.app.controller.UserController;
import org.crazyit.app.domain.User;
import org.crazyit.app.redis.AccessKey;
import org.crazyit.app.redis.FkRedisUtil;
import org.crazyit.app.redis.UserKey;
import org.crazyit.app.result.CodeMsg;
import org.crazyit.app.result.Result;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

@Component
public class AccessInterceptor implements HandlerInterceptor
{
	private final UserController userController;
	private final FkRedisUtil fkRedisUtil;
	public AccessInterceptor(UserController userController,
			FkRedisUtil fkRedisUtil)
	{
		this.userController = userController;
		this.fkRedisUtil = fkRedisUtil;
	}

	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception
	{
		// Gets or creates the ID of the distributed Session
		CookieUtil.getSessionId(request, response);
		User user = getUser(request, response);
		// Store the read User information into the ThreadLocal container of UserContext
		UserContext.setUser(user);
		if (handler instanceof HandlerMethod)
		{
			HandlerMethod hm = (HandlerMethod) handler;
			// Get the @ AccessLimit annotation on the called method
			AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
			// If there is no @ AccessLimit annotation, it directly returns true (release)
			if (accessLimit == null)
			{
				return true;
			}
			int seconds = accessLimit.seconds();
			int maxCount = accessLimit.maxCount();
			boolean needLogin = accessLimit.needLogin();
			String key = request.getRequestURI();
			// If needLogin is true, it indicates that login is required to call this method
			if (needLogin)
			{
				// If the user is null, it indicates that the user is also logged in, and the call is directly rejected
				if (user == null)
				{
					render(response, CodeMsg.SESSION_ERROR);
					return false;
				}
			}
			// If the properties seconds and maxCount are set,
			// Indicates that the specified method can only be called several times within the specified time
			if (seconds > 0 && maxCount > 0)
			{
				key += "_" + user.getId();
				AccessKey ak = AccessKey.withExpire(seconds);
				// Take ak as the prefix and add the user's mobile phone number as the real key to obtain the number of visits
				Integer count = fkRedisUtil.get(ak, key, Integer.class);
				// If count is null, it indicates that it has not been accessed before
				if (count == null)
				{
					fkRedisUtil.set(ak, key, 1);
				}
				// If the number of visits has not reached the maximum, you can continue to visit
				else if (count < maxCount)
				{
					// Number of visits plus 1
					fkRedisUtil.incr(ak, key);
				}
				// If the number of visits reaches the limit
				else
				{
					// Generate error prompt
					render(response, CodeMsg.ACCESS_LIMIT_REACHED);
					return false;
				}
			}
		}
		return true;
	}
	// This method is used to generate an error response based on CodeMsg
	private void render(HttpServletResponse response,
			CodeMsg cm) throws IOException
	{
		response.setContentType("application/json;charset=UTF-8");
		OutputStream out = response.getOutputStream();
		// Wrap CodeMsg into a Result object and convert it into a string
		String str = FkRedisUtil.beanToString(Result.error(cm));
		// Output response string
		out.write(str.getBytes(StandardCharsets.UTF_8));
		out.flush();
		out.close();
	}

	private User getUser(HttpServletRequest request, HttpServletResponse response)
	{
		// Get the request parameter named token
		String paramToken = request.getParameter(UserKey.COOKIE_NAME_TOKEN);
		// Gets the value of the Cookie named token
		String cookieToken = CookieUtil.getCookieValue(request, UserKey.COOKIE_NAME_TOKEN);
		if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken))
		{
			return null;
		}
		// paramToken is preferred as the ID of the distributed Session
		String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
		// Obtain the Session object according to the distributed Session ID
		return userController.getByToken(response, token);
	}
}

The getUser() method will read the User information from Redis, that is, from the distributed Session, and store the User information read from the distributed Session into the ThreadLocal container of UserContext. UserArgumentResolver is still reading User information from distributed Session when parsing User parameters.

3. Access control

There is an @ AccessLimit annotation on the list() method of ItemController. This annotation has the function of permission control. The method modified by this annotation can only be called after logging in by default, and this annotation can limit how many times the modified method can only be called for a specified user in a specific time. In this way, it can not only limit the repeated second kill of the same user, but also avoid the concurrency peak caused by multiple second kills of users.

@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit
{
    boolean needLogin() default true;
    // This annotation limits the maximum number of times the decorated method can be accessed within a specified time
    // -1 means unlimited
    int seconds() default -1;
    int maxCount() default -1;
}

@The AccessLimit annotation can be used to decorate methods, and the annotation can be retained until run time. The annotation supports the following three attributes:

  • needLogin: this attribute specifies whether the modified method needs repeated login to be called. The default value of this attribute is true
  • Seconds: this attribute specifies the number of seconds
  • maxCount: this attribute specifies that the same user can only call the modified method maxCount times at most within the time limited by Seconds
			// If needLogin is true, it indicates that login is required to call this method
			if (needLogin)
			{
				// If the user is null, it indicates that the user is also logged in, and the call is directly rejected
				if (user == null)
				{
					render(response, CodeMsg.SESSION_ERROR);
					return false;
				}
			}

The user must log in to call the modified method, otherwise the method returns CodeMsg.SESSION_ERROR error prompt.

				// If count is null, it indicates that it has not been accessed before
				if (count == null)
				{
					fkRedisUtil.set(ak, key, 1);
				}
				// If the number of visits has not reached the maximum, you can continue to visit
				else if (count < maxCount)
				{
					// Number of visits plus 1
					fkRedisUtil.incr(ak, key);
				}
				// If the number of visits reaches the limit
				else
				{
					// Generate error prompt
					render(response, CodeMsg.ACCESS_LIMIT_REACHED);
					return false;
				}

Limit the maximum number of times a user can call the decorated method within a specified time

4. Seckill product page template

\miaosha\src\main\resources\templates\item_list.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
	<meta name="author" content="Yeeku.H.Lee(CrazyIt.org)"/>
	<meta charset="UTF-8">
	<title>Product list</title>
	<!-- jQuery -->
	<script type="text/javascript" th:src="@{/jquery/jquery.min.js}"></script>
	<!-- BootStrap -->
	<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
	<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
</head>
<body>
<div class="container">
	<div class="card">
		<div class="card-header"><h4>Second kill product list</h4></div>
		<div class="card-body">
			<table class="table table-hover" id="itemList">
				<tr>
					<td>Trade name</td>
					<td>Product picture</td>
					<td>Original price of goods</td>
					<td>price spike</td>
					<td>Inventory quantity</td>
					<td>details</td>
				</tr>
				<tr th:each="item, itemStat : ${itemList}">
					<td th:text="${item.itemName}"></td>
					<td>
						<img th:src="@{'/imgs/'+ ${item.itemImg}}"
							 width="80" height="100"/>
					</td>
					<td th:text="${item.itemPrice}"></td>
					<td th:text="${item.miaoshaPrice}"></td>
					<td th:text="${item.stockCount}"></td>
					<td>
						<a th:href="'/item/item_detail.html?itemId='
							+ ${item.itemId}">seckill</a>
					</td>
				</tr>
			</table>
		</div>
	</div>
</div>
</body>
</html>

10, Realization and static of commodity second kill interface

When entering the seckill interface, directly access the static HTML page, and the client browser can automatically cache the static page to avoid repeated loading of HTML pages. The dynamic update part is made into a Restful response, and then let the static page load the content that needs dynamic update asynchronously through jQuery, so that the response of each request is only dynamically updated data, Instead of a full HTML page.

1. Obtain second kill goods

	@GetMapping(value = "/detail/{itemId}")
	@ResponseBody
	@AccessLimit // Restrict that this method must be logged in to access
	public Result<ItemDetailVo> detail(User user,
			@PathVariable("itemId") long itemId)
	{
		MiaoshaItem item = miaoshaService.getMiaoshaItemById(itemId);
		// Get the start time of second kill
		long startAt = item.getStartDate().getTime();
		// Get the end time of the second kill
		long endAt = item.getEndDate().getTime();
		long now = System.currentTimeMillis();
		// A variable that defines how long before the start of the second kill
		int remainSeconds;
		if (now < startAt)
		{
			// The second kill hasn't started yet
			remainSeconds = (int) ((startAt - now) / 1000);
		}
		else if (now > endAt)
		{
			// The second kill is over
			remainSeconds = -1;
		}
		else
		{
			// Second kill in progress
			remainSeconds = 0;
		}
		// A variable that defines how long the second kill ends
		var leftSeconds = (int) ((endAt - now ) / 1000);
		// Create ItemDetailVo to encapsulate the details of seckill products
		ItemDetailVo itemDetailVo = new ItemDetailVo();
		itemDetailVo.setMiaoshaItem(item);
		itemDetailVo.setUser(user);
		itemDetailVo.setRemainSeconds(remainSeconds);
		itemDetailVo.setLeftSeconds(leftSeconds);
		return Result.success(itemDetailVo);
	}

The detail() method calls the getMiaoshaItemById() method of MiaoshaService to obtain the details of spike goods.

// Method for obtaining second kill commodity according to commodity ID
	public MiaoshaItem getMiaoshaItemById(long itemId)
	{
		return miaoshaItemMapper.findById(itemId);
	}

getMiaoshaItemById() is implemented by simply calling the findById() method of MiaoshaItemMapper component.

The detail() processing method needs to transfer the current User information to the page, so an additional ItemDetailVo class is defined to encapsulate MiaoshaItem and User.

import org.crazyit.app.domain.MiaoshaItem;
import org.crazyit.app.domain.User;

public class ItemDetailVo
{
	private int remainSeconds = 0;
	private int leftSeconds = 0;
	private MiaoshaItem miaoshaItem;
	private User user;

	public int getRemainSeconds()
	{
		return remainSeconds;
	}

	public void setRemainSeconds(int remainSeconds)
	{
		this.remainSeconds = remainSeconds;
	}

	public int getLeftSeconds()
	{
		return leftSeconds;
	}

	public void setLeftSeconds(int leftSeconds)
	{
		this.leftSeconds = leftSeconds;
	}

	public MiaoshaItem getMiaoshaItem()
	{
		return miaoshaItem;
	}

	public void setMiaoshaItem(MiaoshaItem miaoshaItem)
	{
		this.miaoshaItem = miaoshaItem;
	}

	public User getUser()
	{
		return user;
	}

	public void setUser(User user)
	{
		this.user = user;
	}

	@Override
	public String toString()
	{
		return "ItemDetailVo{" +
				"remainSeconds=" + remainSeconds +
				", leftSeconds=" + leftSeconds +
				", miaoshaItem=" + miaoshaItem +
				", user=" + user +
				'}';
	}
}

2. Implementation of second kill interface

<!DOCTYPE html>
<html lang="zh">
<head>
	<meta name="author" content="Yeeku.H.Lee(CrazyIt.org)"/>
	<meta charset="UTF-8">
	<title>Product details</title>
	<!-- jQuery -->
	<script type="text/javascript" src="/jquery/jquery.min.js"></script>
	<!-- BootStrap -->
	<link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css"/>
	<script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
	<!-- Layer -->
	<script type="text/javascript" src="/layer/layer.js"></script>
	<!-- Custom common.js -->
	<script type="text/javascript" src="/js/common.js"></script>
	<script>
		// When the page is loaded, execute the following code to obtain the product details
		$(function () {
			// Get itemId query parameter
			let itemId = g_getQueryString("itemId");
			// Send asynchronous request to get the details of second kill products
			$.get("/item/detail/" + itemId, function (data) {
				if (data.code == 0) {
					render(data.data);
				} else {
					layer.msg(data.msg);
				}
			});
		});
		// Dynamically update the page content according to the data responded by the server
		function render(detail) {
			// Get the information of second kill items
			let item = detail.miaoshaItem;
			let user = detail.user;
			if (user) {
				$("#userTip").hide();
			}
			// Dynamic update page
			$("#itemName").text(item.itemName);
			$("#title").text(item.title);
			$("#itemImg").attr("src", "/imgs/" + item.itemImg);
			$("#startTime").text(new Date(item.startDate).format("yyyy-MM-dd hh:mm:ss"));
			$("#remainSeconds").val(detail.remainSeconds);
			$("#leftSeconds").val(detail.leftSeconds);
			$("#itemId").val(item.itemId);
			$("#itemPrice").text(item.itemPrice);
			$("#miaoshaPrice").text(item.miaoshaPrice);
			$("#itemDetail").html(item.itemDetail);
			$("#stockCount").text(item.stockCount);
			// Turn on the countdown
			countDown();
		}
		// Function that defines the countdown
		function countDown() {
			// Get the remaining time from the start of the second kill
			let remainSeconds = $("#remainSeconds").val();
			// Get the time remaining for the end of the second kill
			let leftSeconds = $("#leftSeconds").val();
			// The second kill hasn't started yet
			if (remainSeconds > 0) {
				// Disable the second kill button
				$("#buyButton").attr("disabled", true);
				// Show countdown
				$("#miaoshaTip").html(" second kill has not started, Countdown: "+ g_secs2our (remainseconds));
				// Reduce the countdown by 1 second
				$("#remainSeconds").val(remainSeconds - 1);
				$("#leftSeconds").val(leftSeconds - 1)
				// Call the countDown() function again after 1 second
				setTimeout(countDown, 1000);
			}
			// Second kill in progress
			else if (remainSeconds == 0) {
				// Show countdown
				$("#miaoshaTip").html(" second kill in progress, remaining time: "+ g_secs2our (leftseconds));
				// Reduce the countdown by 1 second
				$("#leftSeconds").val(leftSeconds - 1)
				if (leftSeconds - 1 <= 0)
				{
					// Setting remainSeconds to - 1 indicates that the second kill is over
					$("#remainSeconds").val(-1);
				}
				// Call the countDown() function again after 1 second
				setTimeout(countDown, 1000);
				if ($("#buyButton").attr("disabled")){
					// Enable second kill button
					$("#buyButton").attr("disabled", false);
				}
				if ($("#verifyCodeImg").is(":hidden")) {
					// Display verification code picture
					$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?itemId="
						+ $("#itemId").val()  + "&timestamp=" + new Date().getTime());
					$("#verifyCodeImg").show();
				}
				if ($("#verifyCode").is(":hidden"))
				{
					// Display verification code input box
					$("#verifyCode").show();
				}
			}
			// The second kill is over
			else {
				// Disable the second kill button
				$("#buyButton").attr("disabled", true);
				$("#miaoshaTip").html(" second kill is over ");
				// Hide captcha image
				$("#verifyCodeImg").hide();
				// Hide verification code input box
				$("#verifyCode").hide();
			}
		}
		// Define the function to get the second kill address
		// The seckill system needs to hide the seckill address of goods, so it needs to dynamically generate the seckill address for each commodity
		function getMiaoshaPath() {
			g_showLoading();
			let itemId = $("#itemId").val();
			$.get("/miaosha/path", {
				itemId: itemId,
				verifyCode: $("#verifyCode").val()
			}, function (data) {
				if (data.code == 0) {
					let path = data.data;
					// Execute second kill
					proMiaosha(path);
				} else {
					layer.msg(data.msg);
				}
			});
		}
		// Submit a second kill request and execute a second kill
		function proMiaosha(path) {
			// Send second kill request
			$.post("/miaosha/" + path + "/proMiaosha", {
				itemId: $("#itemId").val()
			}, function (data) {
				// When the second kill is completed (just add the second kill request to the RabbitMQ queue)
				if (data.code == 0) {
					// Call getMiaoshaResult() function to get the second kill result
					getMiaoshaResult($("#itemId").val());
				} else {
					layer.msg(data.msg);
				}
			});
		}
		// Get second kill results
		function getMiaoshaResult(itemId) {
			$.get("/miaosha/result", {
				itemId: $("#itemId").val()
			}, function (data) {
				if (data.code == 0) {
					let result = data.data;
					// If the second kill fails
					if (result < 0) {
						layer.msg("Sorry, second kill failed");
					// The second kill has not been completed. Request again after 0.1 seconds
					} else if (result == 0) {
						// Request again after 0.1
						setTimeout(function () {
							getMiaoshaResult(itemId);
						}, 100);
					// Second kill success
					} else {
						// A confirmation box pops up
						layer.confirm("Congratulations, second kill succeeded! Check the order?", {btn: ["determine", "cancel"]},
							function () {
								// Go to the order details page
								window.location.href = "/order/order_detail.html?orderId=" + result;
							}),
							function () {
								layer.closeAll();
							};
					}
				} else {
					layer.msg(data.msg);
				}
			});
		}
		// Function to refresh verification code
		function refreshVerifyCode() {
			$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?itemId="
				+ $("#itemId").val() + "&timestamp=" + new Date().getTime());
		}
	</script>
</head>
<body>
<div class="container">
	<img src="/imgs/logo.png"
		 class="rounded mx-auto d-block" alt="logo"><h4>Second kill product details</h4>
	<div class="row">
		<div class="col-lg-3"><img id="itemImg" width="240" height="340"></div>
		<div class="col-lg-9 p-3">
			<div class="row py-1 pl-5">
				<div class="col-lg"><h3 id="itemName"></h3></div>
			</div>
			<div class="row py-1 pl-5">
				<div class="col-lg font-weight-bold text-danger" id="title"></div>
			</div>
			<div class="row py-1 pl-5">
				<div class="col-lg">original price:<span class="col-lg" id="itemPrice"></span></div>
			</div>
			<div class="row py-1 pl-5">
				<div class="col-lg">price spike:<span class="col-lg text-danger"
											  id="miaoshaPrice"></span></div>
			</div>
			<div class="row py-1 pl-5">
				<div class="col-lg">Inventory quantity:<span class="col-lg"
											   id="stockCount"></span></div>
			</div>
			<div class="row py-1 pl-5">
				<div class="col-lg">start time:<span class="col-lg"
											   id="startTime"></span></div>
			</div>
			<div class="row py-1 pl-5">
				<div class="col-lg">
					<input type="hidden" id="remainSeconds"/>
					<input type="hidden" id="leftSeconds"/>
					<span id="miaoshaTip" style="color:red;font-weight: bolder"></span>
				</div>
			</div>
			<div class="row py-1 pl-5">
				<div class="col-lg" id="itemDetail"></div>
			</div>
		</div>
	</div>
	<div class="row">
		<div class="col-lg">
			<div class="form-inline justify-content-center">
				<img id="verifyCodeImg" width="80" height="32" style="display:none"
					 onclick="refreshVerifyCode()"/>
				<input id="verifyCode" class="form-control" style="display:none"
					   placeholder="Please enter the verification code"/>
				<button class="btn btn-primary" type="button" id="buyButton"
						onclick="getMiaoshaPath()">Instant spike
				</button>
				<input type="hidden" name="itemId" id="itemId"/>
			</div>
		</div>
	</div>
</div>
</body>
</html>

11, Spike implementation and concurrent peak clipping using RabbitMQ

package org.crazyit.app.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import org.crazyit.app.access.AccessLimit;
import org.crazyit.app.domain.MiaoshaItem;
import org.crazyit.app.domain.MiaoshaOrder;
import org.crazyit.app.domain.User;
import org.crazyit.app.rabbitmq.MiaoshaMessage;
import org.crazyit.app.rabbitmq.MiaoshaSender;
import org.crazyit.app.redis.FkRedisUtil;
import org.crazyit.app.redis.ItemKey;
import org.crazyit.app.result.CodeMsg;
import org.crazyit.app.result.Result;
import org.crazyit.app.service.MiaoshaService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
@RequestMapping("/miaosha")
public class MiaoshaController implements InitializingBean
{
	private final MiaoshaService miaoshaService;
	private final FkRedisUtil fkRedisUtil;
	private final MiaoshaSender mqSender;
	// The corresponding relationship between the stored ItemId and whether the second kill is over
	private final Map<Long, Boolean> localOverMap =
			Collections.synchronizedMap(new HashMap<>());

	public MiaoshaController(MiaoshaService miaoshaService,
			FkRedisUtil fkRedisUtil, MiaoshaSender mqSender)
	{
		this.miaoshaService = miaoshaService;
		this.fkRedisUtil = fkRedisUtil;
		this.mqSender = mqSender;
	}

	@Override
	public void afterPropertiesSet()
	{
		// Get a list of all items
		List<MiaoshaItem> itemList = miaoshaService.listMiaoshaItem();
		if (itemList == null)
		{
			return;
		}
		for (MiaoshaItem item : itemList)
		{
			// Put all items and their corresponding inventory into Redis cache
			fkRedisUtil.set(ItemKey.miaoshaItemStock, ""
					+ item.getItemId(), item.getStockCount());
			localOverMap.put(item.getId(), false);
		}
	}

	@GetMapping(value = "/verifyCode")
	@ResponseBody
	@AccessLimit // Restrict that this method must be logged in to access
	public void getMiaoshaVerifyCode(HttpServletResponse response,
			User user, @RequestParam("itemId") long itemId) throws IOException
	{
		// Generate verification code
		BufferedImage image = miaoshaService.createVerifyCode(user, itemId);
		OutputStream out = response.getOutputStream();
		// Output verification code to client
		ImageIO.write(image, "JPEG", out);
		out.flush();
		out.close();
	}

	@GetMapping(value = "/path")
	@ResponseBody
	// Restrict that this method must be logged in to access, and can only be called 5 times every 5 seconds
	@AccessLimit(seconds = 5, maxCount = 5)
	public Result<String> getMiaoshaPath(User user,
			@RequestParam("itemId") long itemId,
			@RequestParam(value = "verifyCode",
					defaultValue = "0") int verifyCode)
	{
		// If the verification codes entered do not match
		if (!miaoshaService.checkVerifyCode(user, itemId, verifyCode))  // ①
		{
			return Result.error(CodeMsg.REQUEST_ILLEGAL);
		}
		String path = miaoshaService.createMiaoshaPath(user, itemId);
		return Result.success(path);
	}

	@PostMapping("/{path}/proMiaosha")
	@ResponseBody
	@AccessLimit // Restrict that this method must be logged in to access
	public Result<Integer> proMiaosha(Model model, User user,
			@RequestParam("itemId") long itemId,
			@PathVariable("path") String path)
			throws JsonProcessingException
	{
		model.addAttribute("user", user);
		// Verify whether the dynamic seckill address is correct
		boolean check = miaoshaService.checkPath(user, itemId, path);   // ②
		if (!check)
		{
			return Result.error(CodeMsg.REQUEST_ILLEGAL);
		}
		// Quickly obtain whether the second kill of the commodity is over through memory
		Boolean over = localOverMap.get(itemId);
		// If the second kill is over
		if (over != null && over)  // ③
		{
			return Result.error(CodeMsg.MIAO_SHA_OVER);
		}
		// Pre reduced inventory
		long stock = fkRedisUtil.decr(ItemKey.miaoshaItemStock, "" + itemId);
		// If the inventory is less than 0, record the end of the second kill of the commodity in memory and return the prompt of the end of the second kill
		if (stock < 0)
		{
			localOverMap.put(itemId, true);
			return Result.error(CodeMsg.MIAO_SHA_OVER);
		}
		// Obtain the second kill order according to the user ID and commodity ID
		MiaoshaOrder miaoshaOrder = miaoshaService
				.getMiaoshaOrderByUserIdAndItemId(user.getId(), itemId); // ④
		// If the user has a second kill order for the product, it is judged as repeated second kill
		if (miaoshaOrder != null)
		{
			return Result.error(CodeMsg.REPEATE_MIAOSHA);
		}
		// Send message to RabbitMQ message queue
		var miaoshaMessage = new MiaoshaMessage();
		miaoshaMessage.setUser(user);
		miaoshaMessage.setItemId(itemId);
		// Put the second kill message in the queue
		mqSender.sendMiaoshaMessage(miaoshaMessage);  // ⑤
		return Result.success(0);
	}

	/**
	 * Get order status
	 * Return orderId: successful
	 * Return - 1: second kill failed
	 * Return 0: queued
	 */
	@GetMapping(value = "/result")
	@ResponseBody
	@AccessLimit // Restrict that this method must be logged in to access
	public Result<Long> miaoshaResult(Model model, User user,
		@RequestParam("itemId") long itemId)
	{
		model.addAttribute("user", user);
		// Call the getMiaoshaResult() method of MiaoshaService to get the second kill result
		long result = miaoshaService.getMiaoshaResult(user.getId(), itemId);
		return Result.success(result);
	}
}

The controller class implements the InitializingBean interface. The controller will automatically execute the afterpropertieset() method defined in the interface after the dependency is injected. This method will call the listMiaoshaItem() method of MiaoshaService to obtain all second kill products, then traverse each product, and finally load the inventory of all second kill products into Redis.

	// Generate second kill graphic verification code
	public BufferedImage createVerifyCode(User user, long itemId)
	{
		if (user == null || itemId <= 0)
		{
			return null;
		}
		Random rdm = new Random();
		String verifyCode = VercodeUtil.generateVerifyCode(rdm);
		int rnd = VercodeUtil.calc(verifyCode);
		// Save the value of the verification code in Redis
		fkRedisUtil.set(MiaoshaKey.miaoshaVerifyCode,
				user.getId() + "," + itemId, rnd);
		// Returns the generated picture
		return VercodeUtil.createVerifyImage(verifyCode, rdm);
	}

Keywords: Spring Boot

Added by justinede on Wed, 06 Oct 2021 22:35:40 +0300