SpringBoot秒杀商城

SpringBoot秒杀商城

SpringBoot并非什么新的框架,SpringBoot就是Spring + Boot,如同Maven整合了所有的jar包一样,SpringBoot整合了所有框架,并通过main函数启动。

开发设计

1.1 项目建立

  • maven-archetype-quickstart建立SpringBoot项目



- 引入parent依赖与starter依赖
1
2
3
4
5
6
7
8
9
10
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 加入注解使得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
    @EnableAutoConfiguration
    @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
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE generatorConfiguration
    PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
    "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
    <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命令并执行

出现如下报错,是数据库版本问题。CSDN

1
Unknown system variable 'query_cache_size'


  • 注意把table中的几个字段设置为自动生成false后可以删除掉生成的Example

  • 在application下配置

    1
    2
    3
    4
    5
    6
    7
    8
    spring.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
    @SpringBootApplication(scanBasePackages = {"com.seckillmall"})
    @RestController
    @MapperScan("com.seckillmall.dao")
    public class App
    {
    @Autowired
    private UserDOMapper userDOMapper;

    @RequestMapping("/")
    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
    @Controller("user")
    @RequestMapping("/user")
    public class UserController {

    @Autowired
    private UserServiceImpl userService;

    @RequestMapping("/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
    @Service
    public class UserServiceImpl implements UserService {

    @Autowired
    private UserDOMapper userDOMapper;

    //缺乏password的查询返回方法

    @Override
    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
    @Service
    public class UserServiceImpl implements UserService {

    @Autowired
    private UserDOMapper userDOMapper;

    @Autowired
    private UserPasswordMapper userPasswordMapper;

    @Override
    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
    @Service
    public class UserServiceImpl implements UserService {

    @Autowired
    private UserDOMapper userDOMapper;

    @Autowired
    private UserPasswordMapper userPasswordMapper;

    @Override
    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
    @Controller("user")
    @RequestMapping("/user")
    public class UserController {

    @Autowired
    private UserServiceImpl userService;

    @RequestMapping("/get")
    @ResponseBody
    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
    12
    public 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
    7
    public 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
    33
    public 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;

    @Override
    public int getErrCode() {
    return this.errCode;
    }

    @Override
    public String getErrMsg() {
    return this.errMsg;
    }

    @Override
    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);
    }

    @Override
    public int getErrCode() {
    return this.commonError.getErrCode();
    }

    @Override
    public String getErrMsg() {
    return this.commonError.getErrMsg();
    }

    @Override
    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
    @RequestMapping("/get")
    @ResponseBody
    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
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.OK)//屏蔽tomcat自己的处理
    @ResponseBody
    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
    19
    public class BaseController {
    //定义ExceptionHandler解决未被controller层吸收的exception
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.OK)//屏蔽tomcat自己的处理
    @ResponseBody
    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
    @RequestMapping("/getotp")
    @ResponseBody
    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
    13
    public 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
    @Override
    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
    3
    public static final String CONTENT_TYPE_FORMED = "application/x-www-form-urlencoded";
    ···
    @RequestMapping(value = "/getotp", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
  • 然后处理ajax的跨域请求的问题为在controller上加入@CrossOrigin即可

  • 对应的 xhrFields:{withCredentials:true}前端设置也是为了配合使用而设置的
  • 注意BASE64的用法在JDK9之后发生了变化
    1
    2
    3
    4
    5
    6
    7
    8
    public 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
    @Component
    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;
    }

    @Override
    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
    @Service
    public class ItemServiceImpl implements ItemService {

    @Override
    @Transactional
    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
    81
    Service
    public class ItemServiceImpl implements ItemService {

    @Autowired
    private ValidatorImpl validator;

    @Autowired
    private ItemDOMapper itemDOMapper;

    @Autowired
    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;
    }

    @Override
    @Transactional
    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());
    }

    @Override
    public List<ItemModel> listItem() {

    return null;
    }

    @Override
    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
2
3
//分库分表路由信息
Integer userId = 1000122;
userId % 100 // 100个库的100个表里

这里还有一个问题就是对于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
    20
    public 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PromoModel {

private Integer id;

//秒杀活动状态:1表示未开始,2表示进行中,3表示已经结束
private Integer status;

//秒杀活动名称
private String promoName;

//秒杀活动的开始时间
private DateTime startDate;

//结束时间
private DateTime endDate;

//秒杀活动的适用商品
private Integer itemId;

//秒杀商品的活动折扣价
private BigDecimal promoItemPrice;
  • 这是改进后的新模型
  • 然后在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)其他问题的思考

    • 针对过载保护,限流令牌之类的流量削峰平滑操作措施;
    • 防刷操作