SpringBoot秒杀商城
SpringBoot并非什么新的框架,SpringBoot就是Spring + Boot,如同Maven整合了所有的jar包一样,SpringBoot整合了所有框架,并通过main函数启动。
开发设计
1.1 项目建立
- maven-archetype-quickstart建立SpringBoot项目
- 引入parent依赖与starter依赖
1 | <parent> |
加入注解使得SpringBoot开始运行
1
2
3
4
5
6
7
8
9@EnableAutoConfiguration # Spring化
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
SpringApplication.run(App.class, args); # 将App变成bean的形式
}
}增加rest式外部访问流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController # rest访问控制
public class App
{
@RequestMapping("/") # rest映射
public String home(){
return "Hello, World!";
}
public static void main( String[] args )
{
System.out.println( "Hello World!" );
SpringApplication.run(App.class, args);
}
}
1.2 文件配置
- 加入application.propertites配置文件就可以配置端口等
配置数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>在application.properties中加入mybatis.mapper-locations=classpath:mapping/*.xml支持
引入mybatis的自动生成插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>mybatis generator</id>
<phase>package</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<!--允许移动生成文件-->
<verbose>true</verbose>
<!--允许自动覆盖-->
<!--这个一般企业开发不能覆盖,否则别人的成果会受影响-->
<overwrite>true</overwrite>
<!--这个极其重要-->
<configurationFile>
src/main/resources/mybatis-generator.xml
</configurationFile>
</configuration>
</configuration>
</plugin>mybatis-generator的官方文档的地址
官方文档1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<generatorConfiguration>
<!--JDBC驱动jar包的位置-->
<!--<classPathEntry location="C:/workspace/project/learning/mybatis/lib/mysql-connector-java-5.1.6.jar"/>-->
<context id="default" targetRuntime="MyBatis3">
<!--创建Java类时是否取消生成注释-->
<commentGenerator>
<property name="suppressDate" value="true"/>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!--JDBC数据库连接-->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/test"
userId="root"
password="dev">
</jdbcConnection>
<!--
Model模型生成器,用来生成含有主键key的类,记录类 以及查询Example类
targetPackage 指定生成的model生成所在的包名
targetProject 指定在该项目下所在的路径
-->
<javaModelGenerator targetPackage="dulk.learn.mybatis.generator.pojo"
targetProject="src/main/java">
<!-- 是否允许子包,即targetPackage.schemaName.tableName -->
<property name="enableSubPackages" value="true"/>
<!-- 是否对model添加构造函数 -->
<property name="constructorBased" value="true"/>
<!-- 是否对类CHAR类型的列的数据进行trim操作 -->
<property name="trimStrings" value="true"/>
<!-- 建立的Model对象是否 不可改变 即生成的Model对象不会有 setter方法,只有构造方法 -->
<property name="immutable" value="false"/>
</javaModelGenerator>
<!--
mapper映射文件生成所在的目录 为每一个数据库的表生成对应的SqlMap文件
-->
<sqlMapGenerator targetPackage="generator"
targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!--
客户端代码,生成易于使用的针对Model对象和XML配置文件的代码
type="ANNOTATEDMAPPER",生成Java Model和基于注解的Mapper对象
type="MIXEDMAPPER",生成基于注解的Java Model和相应的Mapper对象
type="XMLMAPPER",生成SQLMap XML文件和独立的Mapper接口
-->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.xxx.dao"
targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!--tables表及类名-->
<table tableName="author" domainObjectName="Author"
enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false">
</table>
<table tableName="book" domainObjectName="Book"
enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false">
</table>
</context>
</generatorConfiguration>
这是一种比较详细的配置方式。
2.1 表结构设计及自动生成
- 1)密码和表结构是分开设计的,为user_info与user_password,且密码要加密存入
- 2)设置好plugins和具体的配置文件之后,即可通过generator生成一个新的mybatis文件,注意tables等指标都要重新设计
- 3)之后在Run的配置栏中选中edit configuration配置,新建maven,然后配置mybatis-generator命令并执行
出现如下报错,是数据库版本问题。CSDN1
Unknown system variable 'query_cache_size'
注意把table中的几个字段设置为自动生成false后可以删除掉生成的Example
在application下配置
1
2
3
4
5
6
7
8spring.datasource.name=seckillmall
spring.datasource.url=jdbc:mysql://xxxx:3306/seckillmall
spring.datasource.data-username="xxx"
spring.datasource.data-password="xxx"
# 使用druid数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
2.2 修改App主入口
加入spring新注解扫描,替换之前的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18(scanBasePackages = {"com.seckillmall"})
("com.seckillmall.dao")
public class App
{
private UserDOMapper userDOMapper;
("/")
public String home(){
UserDO userDO = userDOMapper.selectByPrimaryKey(1);
if(userDO == null){
return "用户不存在";
}else{
return userDO.getName();
}
}
}注:这里有一个BUG,Mybatis的自动生成覆写很智障!!每次都会在头部插入新的代码段,需要手动控制,不然会报resultMap错误。
- *注2:这里还有一个问题,Druid的配置过程中参数一定要写对,有的时候是spring.datasource.username而不是spring.datasource.data-username
- 注3:报错Communications link failure的时候直接刷新一下重启就好了,可能是TCP连接的重建问题。
3.1 开发用户信息业务逻辑
- 1)先构建出UserController和UserService这些MVC常见组件,用于实现查询业务逻辑;
controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14("user")
("/user")
public class UserController {
private UserServiceImpl userService;
("/get")
public void getUser(@RequestParam(name="id")Integer id){
//调用service服务并获取对象
UserModel userModel = userService.getUserById(id);
}
}2)这里引入一个重要概念是UserModel用于实现对前端的交互,组装了info和passward信息,因为Dao层的东西是不能透传给前端的;注意还要构建Model的从Dao转化到Model的方法
UserModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class UserServiceImpl implements UserService {
private UserDOMapper userDOMapper;
//缺乏password的查询返回方法
public UserModel getUserById(Integer id) {
UserDO userDO = userDOMapper.selectByPrimaryKey(id);
//缺乏返回
return null;
}
private UserModel convertFromDataObject(UserDO userDO, UserPassword userPassword){
UserModel userModel = new UserModel();
if(userDO == null){
return null;
}
BeanUtils.copyProperties(userDO,userModel);
if(userPassword != null){
userModel.setEncrptPassword(userPassword.getEncrptPassword());
}
}
}3)引入自动化的然后改造Mybatis文件,进行下一步的处理后在service中进行修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class UserServiceImpl implements UserService {
private UserDOMapper userDOMapper;
private UserPasswordMapper userPasswordMapper;
public UserModel getUserById(Integer id) {
//查询对应的业务逻辑
UserDO userDO = userDOMapper.selectByPrimaryKey(id);
//用户不存在
if(userDO == null){
return null;
}
//查询密码等加密信息
UserPassword userPassword = userPasswordMapper.selectByUserId(id);
return convertFromDataObject(userDO, userPassword);
}
private UserModel convertFromDataObject(UserDO userDO, UserPassword userPassword){
UserModel userModel = new UserModel();
if(userDO == null){
return null;
}
BeanUtils.copyProperties(userDO,userModel);
if(userPassword != null){
userModel.setEncrptPassword(userPassword.getEncrptPassword());
}
return userModel;
}
}改进之后的service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class UserServiceImpl implements UserService {
private UserDOMapper userDOMapper;
private UserPasswordMapper userPasswordMapper;
public UserModel getUserById(Integer id) {
//查询对应的业务逻辑
UserDO userDO = userDOMapper.selectByPrimaryKey(id);
//用户不存在
if(userDO == null){
return null;
}
//查询密码等加密信息
UserPassword userPassword = userPasswordMapper.selectByUserId(id);
return convertFromDataObject(userDO, userPassword);
}
private UserModel convertFromDataObject(UserDO userDO, UserPassword userPassword){
UserModel userModel = new UserModel();
if(userDO == null){
return null;
}
BeanUtils.copyProperties(userDO,userModel);
if(userPassword != null){
userModel.setEncrptPassword(userPassword.getEncrptPassword());
}
return userModel;
}
}controller的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16("user")
("/user")
public class UserController {
private UserServiceImpl userService;
("/get")
public UserModel getUser(@RequestParam(name="id")Integer id){
//调用service服务并获取对象
UserModel userModel = userService.getUserById(id);
return userModel;
}
}注:没有写@ResponsBody后就会发现请求会无法显示成为json格式
3.2 重构改进
- 这样直接返回Json格式下用户密码,即便是加密格式下的,也会不是很好的设计方式,因此对它进行改造。
加入UserVO即ViewObject即可观察到最终的情况。
1
2
3
4
5
6
7
8
9
10
11
12public class UserVO {
private Integer id;
private String name;
private Byte gender;
private Integer age;
private String telphone;
}现在的模式不允许任何错误,需要对它进行下一步的调整,需要归一化,建立response.CommonReturnType模式,将业务逻辑错误与服务器错误区分开来
1
2
3
4
5
6
7public class CommonReturnType {
//处理结果有"success"和"fail"
private String status;
private Object data;
}
3.3 错误异常处理
定义枚举类对错误信息进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public enum EmBusinessError implements CommonError {
//通用错误类型00001,解决入参校验
PARAMETER_VALIDATION_ERROR(00001, "参数不合法"),
//10000开头表示为用户信息相关定义错误
User_NOT_EXIST(10001, "用户不存在")
;
EmBusinessError(int errCode, String errMsg) {
this.errCode = errCode;
this.errMsg = errMsg;
}
private int errCode;
private String errMsg;
public int getErrCode() {
return this.errCode;
}
public String getErrMsg() {
return this.errMsg;
}
public CommonError setErrMsg(String errMsg) {
this.errMsg = errMsg;
return this;
}
}处理流程为将所有异常抛到controller上的handler上进行处理,使用包装器模式构建Exception
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34//包装器业务异常类实现
public class BusinessException extends Exception implements CommonError {
//强关联一个Error
private CommonError commonError;
//直接接受EmBusinessError的传参用于构造业务异常
public BusinessException(CommonError commonError) {
super();
this.commonError = commonError;
}
//接受自定义ErrorMsg的方式接受业务异常
public BusinessException(CommonError commonError, String errMsg) {
super();
this.commonError = commonError;
this.setErrMsg(errMsg);
}
public int getErrCode() {
return this.commonError.getErrCode();
}
public String getErrMsg() {
return this.commonError.getErrMsg();
}
public CommonError setErrMsg(String errMsg) {
return this.commonError.setErrMsg(errMsg);
}
}然后在controller中对异常进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14("/get")
public CommonReturnType getUser(@RequestParam(name="id")Integer id) throws BusinessException {
//调用service服务并获取对象
UserModel userModel = userService.getUserById(id);
if(userModel == null){
throw new BusinessException(EmBusinessError.User_NOT_EXIST);
}
//将核心领域模型用户转UI给用户使用的模型
UserVO userVO = convertFromModel(userModel);
return CommonReturnType.create(userVO);
}怎么拦截这个tomcat的异常处理过程显示呢?定义ExceptionHandler来解决未被controller层吸收的exception,采用@ResponseBody模式产生json。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13//定义ExceptionHandler解决未被controller层吸收的exception
(Exception.class)
(HttpStatus.OK)//屏蔽tomcat自己的处理
public Object handlerException(HttpServletRequest request, Exception ex) {
BusinessException exception = (BusinessException) ex;
Map<String, Object> responseData = new HashMap<>();
responseData.put("errCode", exception.getErrCode());
responseData.put("errMsg", exception.getErrMsg());
return CommonReturnType.create(responseData, "fail");
}添加BaseController后我们仍旧可以让UserController去继承该类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class BaseController {
//定义ExceptionHandler解决未被controller层吸收的exception
(Exception.class)
(HttpStatus.OK)//屏蔽tomcat自己的处理
public Object handlerException(HttpServletRequest request, Exception ex) {
Map<String, Object> responseData = new HashMap<>();
if(ex instanceof BusinessException) {
BusinessException exception = (BusinessException) ex;
responseData.put("errCode", exception.getErrCode());
responseData.put("errMsg", exception.getErrMsg());
}else {
responseData.put("errCode", EmBusinessError.UNKNOWN_ERROR.getErrCode());
responseData.put("errMSg", EmBusinessError.UNKNOWN_ERROR.getErrMsg());
}
return CommonReturnType.create(responseData, "fail");
}
}
otp用户注册模块
1.1 用户获取otp短信
首先的一个设计是生成otp代码,这个一般是通过购买第三方的服务实现的,而我们这里采用随机数生成的方式进行。
1
2
3
4//获取otp随机码
Random random = new Random();
int randomCode = random.nextInt(99999);
String optCode = String.valueOf(randomCode + 10000);然后的一个设计为将otp与用户代码进行绑定,这一步一般是使用redis进行操作,这里采用简单的绑定到httpsession中进行处理。
1
2//与Httpsession进行绑定
httpServletRequest.setAttribute(telphone, optCode);最后是输出otpcode,这里使用控制台输出检验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17("/getotp")
public CommonReturnType getOtp(@RequestParam(name="telphone")String telphone){
//获取otp随机码
Random random = new Random();
int randomCode = random.nextInt(99999);
String optCode = String.valueOf(randomCode + 10000);
//与Httpsession进行绑定
httpServletRequest.getSession().setAttribute(telphone, optCode);
//将optcode返回给用户
System.out.println("telphone = "+ telphone + " & optdoe = "+ optCode);
return CommonReturnType.create(null);
}
1.2 用户注册逻辑实现
先校验optcode
1
2
3
4
5
6
7
8
9
10
11
12
13public CommonReturnType register(@RequestParam(name="telphone")String telphone,
@RequestParam(name = "id")Integer id,
@RequestParam(name = "name")String name,
@RequestParam(name = "gender")Byte gender,
@RequestParam(name = "age")Integer age,
@RequestParam(name = "optcode")String optCode
) throws BusinessException {
//首先校验optcode
String inSessionCode = (String)this.httpServletRequest.getSession().getAttribute("telphone");
if(!com.alibaba.druid.util.StringUtils.equals(inSessionCode, optCode)){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "注册短信验证错误");
}
}增加两个对应的转化字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public void register(UserModel userModel) throws BusinessException {
if(userModel == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "用户信息为空");
}
if(org.apache.commons.lang3.StringUtils.isEmpty(userModel.getName())
|| userModel.getGender() == null
|| userModel.getAge() == null
|| org.apache.commons.lang3.StringUtils.isEmpty(userModel.getTelphone())){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
}
//实现model转化为dataobject
UserDO userDO = convertFromUserModel(userModel);
userDOMapper.insertSelective(userDO);
UserPassword userPassword = convertPasswordFromUserModel(userModel);
userPasswordMapper.insertSelective(userPassword);
}
private UserPassword convertPasswordFromUserModel(UserModel userModel){
if(userModel == null){
return null;
}
UserPassword userPassword = new UserPassword();
userPassword.setEncrptPassword(userModel.getEncrptPassword());
userPassword.setId(userModel.getId());
return userPassword;
}
private UserDO convertFromUserModel(UserModel userModel){
UserDO userDO = new UserDO();
if(userModel == null){
return null;
}
BeanUtils.copyProperties(userModel, userDO);
return userDO;
}前后端连接,注意先在其中加上@RequestMethod字段和一个content_type类型
1
2
3public static final String CONTENT_TYPE_FORMED = "application/x-www-form-urlencoded";
···
(value = "/getotp", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})然后处理ajax的跨域请求的问题为在controller上加入@CrossOrigin即可
- 对应的 xhrFields:{withCredentials:true}前端设置也是为了配合使用而设置的
- 注意BASE64的用法在JDK9之后发生了变化
1
2
3
4
5
6
7
8public String enCodeByMD5(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException {
// 确定计算方法
MessageDigest md5 = MessageDigest.getInstance("MD5");
BASE64Encoder base64Encoder = new BASE64Encoder();
// 加密字符串
String newStr = base64Encoder.encode(md5.digest(str.getBytes("utf-8")));
return newStr;
}
解决地址:jdk9之后
- 解决数据库唯一索引问题,添加对手机号的索引之后,就可以添加对于异常的处理过程,在register里catch到
1.3 验证模块的改进
增加依赖
1
2
3
4
5<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.4.Final</version>
</dependency>增加验证过程,使用hibernate自带的用户模块,新建ValidatorImpl,注意加注释能够使它在初始化过程中被扫描到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ValidatorImpl implements InitializingBean {
private Validator validator;
//实现校验方法
public ValidationResult validate(Object bean){
ValidationResult validationResult = new ValidationResult();
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(bean);
if(constraintViolations.size() > 0){
//有错误
validationResult.setHasError(true);
constraintViolations.forEach(constraintViolation -> {
String errMsg = constraintViolation.getMessage();
String propertyName = constraintViolation.getPropertyPath().toString();
validationResult.getErrMsgMap().put(propertyName, errMsg);
});
}
return validationResult;
}
public void afterPropertiesSet() throws Exception {
//将hibernate validator的工厂模式使其实例化
this.validator = Validation.buildDefaultValidatorFactory().getValidator();
}
}然后配合注解的@NotBlank等就可以实现对于其的校验,这是一种hibernate+注解的方式
2.2 登录模块的修改
- 业务逻辑注意有对于Mapper.xml的修改
商品模块的实现
对于一个初级Java程序员来说,设计一个程序可能就是按照用户经理的UI设计去复现表结构,然后使用Mybatis对结构进行调整,但是这样设计是错误的。
一定要先设计领域模型,也就是Model
1.1 商品模块的设计与实现
- 首先修改pom中mybatis自动生成的覆写参数!!
然后在mybatis-generator上修改使其生成新的表参数
列出一个面向领域编程的典型设计service流程,service注解加上,然后Transactional主要保证对一个事务的读写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ItemServiceImpl implements ItemService {
public ItemModel createItem(ItemModel itemModel) {
//校验入参
//转化ItemModel变为DataObject
//写入数据库
//返回创建对象
return null;
}
}Service层尽量写得复杂,Controller层则是尽量写得简单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81Service
public class ItemServiceImpl implements ItemService {
private ValidatorImpl validator;
private ItemDOMapper itemDOMapper;
private ItemStockDOMapper itemStockDOMapper;
private ItemDO convertFromItemModel(ItemModel itemModel){
if(itemModel == null){
return null;
}
ItemDO itemDO = new ItemDO();
BeanUtils.copyProperties(itemModel, itemDO);
itemDO.setPrice(itemModel.getPrice().doubleValue());
return itemDO;
}
private ItemStockDO convertStockFromItemModel(ItemModel itemModel){
if(itemModel == null){
return null;
}
ItemStockDO itemStockDO = new ItemStockDO();
itemStockDO.setId(itemModel.getId());
itemStockDO.setStock(itemModel.getStock());
return itemStockDO;
}
public ItemModel createItem(ItemModel itemModel) throws BusinessException {
//校验入参
ValidationResult validationResult = validator.validate(itemModel);
if(validationResult.isHasErrors()){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, validationResult.getErrMsg());
}
//转化ItemModel变为DataObject
ItemDO itemDO = this.convertFromItemModel(itemModel);
//写入数据库
itemDOMapper.insertSelective(itemDO);
itemModel.setId(itemDO.getId());
ItemStockDO itemStockDO = this.convertStockFromItemModel(itemModel);
itemStockDOMapper.insertSelective(itemStockDO);
//返回创建对象
return this.getItemById(itemModel.getId());
}
public List<ItemModel> listItem() {
return null;
}
public ItemModel getItemById(Integer id) {
ItemDO itemDO = itemDOMapper.selectByPrimaryKey(id);
if(itemDO == null) {
return null;
}
//操作获得库存数量
ItemStockDO itemStockDO = itemStockDOMapper.selectByItemId( itemDO.getId());
//将dataobject转换成model
return this.convertModelFromDataObject(itemDO,itemStockDO);
}
private ItemModel convertModelFromDataObject(ItemDO itemDO,ItemStockDO itemStockDO){
ItemModel itemModel = new ItemModel();
BeanUtils.copyProperties(itemDO,itemModel);
itemModel.setPrice(new BigDecimal(itemDO.getPrice()));
itemModel.setStock(itemStockDO.getStock());
return itemModel;
}顺序是ItemModel -> 生成表结构 -> 写ItemService -> 实现 -> 写Controller层 -> 写ItemVO用于返回给前端
1.3 前端展示商品
- 注意图片的引用格式
订单生成
1.1 订单号生成
首先对订单进行Model设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//解决用户下单的交易模型
public class OrderModel {
//企业级别应用的交易号是要记录时间的明显格式,如20190701+88888
private String id;
//购买的用户id
private Integer userId;
//商品id
private Integer itemId;
//购买商品的单价,这家伙是到时候写秒杀用的东西,变化的时候要用
private BigDecimal itemPrice;
//数目
private Integer amount;
//总金额
private BigDecimal orderAmount;
}使用Mybatis-generator生成相应的Mapper和DO模型;
- 创建OrderService,构建create方法,传入的参数为用户和订单的id,以及相应的数量;
- 构建Service实现以及加上@service标注,重写方法,并保证事务是在同一个订单当中,@Transactional;
- 1)校验用户及商品存在->2)校验商品->3)落单并减库存/支付减库存(更难,且无法避免超卖问题,要造成退单)-> 4)订单入库 -> 5)返回前端
这里有两个可以展开来研究的点:
第一个就是一个落单减库存,虽然是合情合理的,但是可能被恶意下单,从而造成损失;
商家为了让用户及早进行交易的提交,采用支付减库存的方式,但是这样又会遇到超卖退单的问题,用户体验较差,因此需要进行进一步的处理,超卖问题的解决是依靠——备货来解决的,但是要有限度。
第二个就是在落单减库存的时候,我们要对stock进行操作,其实这里完全可以独立出一个Service操作对stock进行进一步的优化,可以展开来写。
详细步骤
- 1)校验有三个信息并抛出异常
- 2)落单要产生新的update sql语句,并且返回int值,根据int值再来减库存,这些都是对于stock表的操作
- 3)注意操作都是在Service层之间互相调用的,隔离开来Mapper
- 4)产生新的订单信息,也要计算金额,并将Model转为DO
- 5)将DO写入数据库
- 6)但是这里id为主键(string)需要生成交易流水号:订单号16位,前八位为时间信息,年月日(用于归档消除数据库使用),中间6位为自增序列(保证订单号不重复,如果超过6位数字还要再增大),最后2位为分库分表位(00到99,对订单进行用户水平拆分)
- 为什么加了Transactional标签后反而private不能写入?
1 | //分库分表路由信息 |
这里还有一个问题就是对于sequence表解决序列数字问题的过程中,还有可能造成超出6位,这时就需要对表进行循环写入。
还有就是事务处理包含在了一个Transactional当中,失败回滚时候,序列号也需要被处理。这时修改get序列号的方法加入新的Transactional参数就可以完成修改。
构建Controller
- 获取用户id
- 检查id后进行商品的订单处理,库存减少
- 调整Model和Dao的数量问题,使其对等(BeanUtils.copyPropertities)
- 对库存进行调整,销量增加
秒杀模块的实现
1.1 构建秒杀活动
构建出来的秒杀活动原型Model
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class PromoModel {
private Integer id;
//秒杀活动名称
private String promoName;
//秒杀活动的开始时间
private DateTime startDate;
//秒杀活动的适用商品
private Integer itemId;
//秒杀商品的活动折扣价
private BigDecimal promoItemPrice;
public Integer getId() {
return id;
}
}注意Mysql数据库中的date数据类型为年月日时分秒类型,即:’0000-01-01 00:00:00’,这也是一个数据库的设置问题,有几种典型的处理问题,连接如下:
Mysql中datetime默认值问题
1.2 改进活动模型
1 | public class PromoModel { |
- 这是改进后的新模型
- 然后在itemModel中加入promoModel进行改进,这使用了聚合模型
- 修改ItemVO显示对应的promo信息,这里也是聚合模型的一种体现
1.2 修改订单模块
- 加入一个promoId表示是否具有对应的秒杀活动。
- 然后对OrderService进行修改,显然第一种可能性更改,而第二种情况下平价商品也需要查询,消耗比较大。
1
2
3//1.通过前端url传入活动id,完成对于id下是否有活动?活动时间?活动是否属于商品?的校验
//2.直接在下单接口内判断对应商品是否存在秒杀活动,若存在进行中的则以秒杀价格下单
OrderModel createOrder(Integer userId, Integer itemId, Integer amount) throws BusinessException;
总结
扩展要求
- 多商品,多库存,多活动模型怎么实现?
秒杀活动业务逻辑实现后,如何支撑亿级流量的实现,比如
- 容量问题及其解决,出现在哪里,怎样压测并发现问题
- 系统水平扩展,怎么做到支持?redis有用吗?
- 查询效率低下(很深层次的问题)
- 活动开始前页面被疯狂刷新(查询次数会很多很大)
- 库存行锁问题(同一时间只有一个事务可以执行减操作,上百都成问题)
- 下单操作多,缓慢(策略问题)
- 浪涌流量如何解决(缓存瞬间失效问题)
参考的解决方案
发现问题
- 当前项目在完成了闭环的业务数据设计后,可以考虑系统的性能问题。现在这种spring加mybatis的框架做数据的crud存取,没有考虑扩展问题和缓存问题。
找到的几种解决方案
1)考虑容器级别的优化:
- tomcat使用了什么样的线程池配置?
- 和客户端是否采用了keepalive的链接?
- 过载保护后的拒绝策略是什么样的?
- 核心线程在idle状态下需要维持多久保证可以扛住洪峰流量?
2)考虑到水平扩展性:
- 单台机器的容量无法扛住所有的压力,考虑系统是否支持无状态部署?(所谓的无状态就是没有什么配置或者数据是依赖于应用服务器的,这样才可以无脑添加机器。)
- 在机器得到扩展后需要有统一的存储来支持数据的访问,分布式的redis会话就可以用来解决数据会话问题。
- 数据库的单点问题也是可以用对应的集中式管理策略来解决
3)考虑到查询压力:
- 考虑使用多级缓存策略解决问题:怎么确保多级缓存可以有效的保障我们的系统?
- 怎么忍受对应的脏读问题?
- 一种方案:最简单的我们可以使用集中式的redis缓存方案解决问题:由于redis支持很好的cluster分片集群模式,其本身又是可以水平扩展的,当redis的网络及内存消耗不能满足热点数据访问时,我们可以使用本地自身实现lru策略的cache缓存,并控制好总容量以及提供快速的响应,并且可以将对应的热点缓存逐步靠近用户,例如可以迁移到nginx甚至于cdn上以求快速的响应用户请求。
4)考虑交易压力的时候:
- 由于交易的所有行为都依赖数据库的落地数据确保正确性,因此需要做一些数据库的锁级别的优化外,针对库存这类热点数据还可以借助缓冲等方式做实现
- 一旦使用了非落地的数据做交易流程,就必须确保数据的最终一致性避免超卖,超发之类的事件发生,因此通常需要设计分布式的最终一致性事务方案解决问题。
5)其他问题的思考
- 针对过载保护,限流令牌之类的流量削峰平滑操作措施;
- 防刷操作