java高并发秒杀项目之Dao

Java高并发秒杀APi之业务分析与DAO层代码编写

具体可以参考github

Maven创建项目seckill

mvn archetype:generate -DgroupId=cn.codingxiaxw.seckill -DartifactId=seckill -Dpackage=cn.codingxiaxw.seckill -Dversion=1.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-webapp

打开WEB-INF下的web.xml,它默认为我们创建servlet版本为2.3,需要修改它的根标签为:






项目架构如下:

java高并发秒杀项目之Dao_第1张图片

pom.xml如下:


  4.0.0
  cn.czy.seckill
  seckill
  war
  1.0-SNAPSHOT
  seckill Maven Webapp
  http://maven.apache.org
  
    
      
      junit
      junit
      4.11
      test
    
    
    
    
      org.slf4j
      slf4j-api
      1.7.12
    
    
      ch.qos.logback
      logback-core
      1.1.1
    
    
    
      ch.qos.logback
      logback-classic
      1.1.1
    
    
    
      mysql
      mysql-connector-java
      5.1.35
      runtime
    
    
      c3p0
      c3p0
      0.9.1.1
    
    
    
      org.mybatis
      mybatis
      3.3.0
    
    
    
      org.mybatis
      mybatis-spring
      1.2.3
    
      
      
          taglibs
          standard
          1.1.2
      
      
          jstl
          jstl
          1.2
      
      
          com.fasterxml.jackson.core
          jackson-databind
          2.5.4
      
      
          javax.servlet
          javax.servlet-api
          3.1.0
      
    
    
    
      org.springframework
      spring-core
      4.1.7.RELEASE
    
    
      org.springframework
      spring-beans
      4.1.7.RELEASE
    
    
      org.springframework
      spring-context
      4.1.7.RELEASE
    
    
    
      org.springframework
      spring-jdbc
      4.1.7.RELEASE
    
    
      org.springframework
      spring-tx
      4.1.7.RELEASE
    
    
    
      org.springframework
      spring-web
      4.1.7.RELEASE
    
    
      org.springframework
      spring-webmvc
      4.1.7.RELEASE
    
    
    
      org.springframework
      spring-test
      4.1.7.RELEASE
    
  
  
    seckill
  

这样项目也就基本构建好了。

业务分析

为什么我们的系统需要事务?看如下这些故障:1.若是用户成功秒杀商品我们记录了其购买明细却没有减库存。导致商品的超卖。2.减了库存却没有记录用户的购买明细。导致商品的少卖。对于上述两个故障,若是没有事务的支持,损失最大的无疑是我们的用户和商家。在MySQL中,它内置的事务机制,可以准确的帮我们完成减库存和记录用户购买明细的过程。

MySQL实现秒杀的难点分析:当用户A秒杀id为10的商品时,此时MySQL需要进行的操作是:1.开启事务。2.更新商品的库存信息。3.添加用户的购买明细,包括用户秒杀的商品id以及唯一标识用户身份的信息如电话号码等。4.提交事务。若此时有另一个用户B也在秒杀这件id为10的商品,他就需要等待,等待到用户A成功秒杀到这件商品然后MySQL成功的提交了事务他才能拿到这个id为10的商品的锁从而进行秒杀,而同一时间是不可能只有用户B在等待,肯定是有很多很多的用户都在等待拿到这个行级锁。秒杀的难点就在这里,如何高效的处理这些竞争?如何高效的完成事务?在后面第4个模块如何进行高并发的优化为大家讲解。

实现功能:

1.秒杀接口的暴露。

2.执行秒杀的操作。

3.相关查询,比如说列表查询,详情页查询。

建数据库时的问题

CREATE TABLE seckill (
  `seckill_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品库存id',
  `name` VARCHAR(120) NOT NULL COMMENT '商品名称',
  `number` INT NOT NULL COMMENT '库存数量',
  `start_time` TIMESTAMP NOT NULL COMMENT '开始时间',
  `end_time` TIMESTAMP NOT NULL COMMENT '结束时间',
  `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (seckill_id),
  KEY idx_start_time(start_time),
  KEY idx_end_time(end_time),
  KEY idx_create_time(create_time)
) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';

When I try to create the table with the previous query, this is the error I get:

ERROR 1067 (42000): Invalid default value for ‘end_time’

答案:

In MySQL, the TIMESTAMP data type differs in nonstandard ways from other data types:

  • TIMESTAMP columns not explicitly declared with the NULL attribute are assigned the NOT NULL attribute. (Columns of other data types, if not explicitly declared as NOT NULL, permit NULL values.) Setting such a column to NULL sets it to the current timestamp.
  • The first TIMESTAMP column in a table, if not declared with the NULL attribute or an explicit DEFAULT or ON UPDATE clause, is automatically assigned the DEFAULT CURRENT_TIMESTAMP and ON UPDATE CURRENT_TIMESTAMP attributes.
  • TIMESTAMP columns following the first one, if not declared with the NULL attribute or an explicit DEFAULT clause, are automatically assigned DEFAULT ‘0000-00-00 00:00:00’ (the “zero” timestamp). For inserted rows that specify no explicit value for such a column, the column is assigned ‘0000-00-00 00:00:00’ and no warning occurs.

Those nonstandard behaviors remain the default for TIMESTAMP but as of MySQL 5.6.6 are deprecated and this warning appears at startup:

[Warning] TIMESTAMP with implicit DEFAULT value is deprecated.
Please use --explicit_defaults_for_timestamp server option 
(see documentation for more details).

在mysql高版本会出现这样的问题,在命令行加上这句话就可以了

set @@session.explicit_defaults_for_timestamp=on;

建完该表后,就set @@session.explicit_defaults_for_timestamp=off;再建下一个表就好了。

在MySQL 5.7版本之前,且在MySQL 5.6.6版本之后(explicit_defaults_for_timestamp参数在MySQL 5.6.6开始加入)的版本中,如果没有设置explicit_defaults_for_timestamp=1的情况下:

1)在默认情况下,如果TIMESTAMP列没有显示的指明null属性,那么该列会被自动加上not null属性(而其他类型的列如果没有被显示的指定not null,那么是允许null值的),如果往这个列中插入null值,会自动的设置该列的值为current timestamp值。

2)表中的第一个TIMESTAMP列,如果没有指定null属性或者没有指定默认值,也没有指定ON UPDATE语句。那么该列会自动被加上DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP属性。

3)第一个TIMESTAMP列之后的其他的TIMESTAMP类型的列,如果没有指定null属性,也没有指定默认值,那么该列会被自动加上DEFAULT ‘0000-00-00 00:00:00’属性。如果insert语句中没有为该列指定值,那么该列中插入’0000-00-00 00:00:00’,并且没有warning。

如果我们在启动的时候在配置文件中指定了explicit_defaults_for_timestamp=1,MySQL会按照如下的方式处理TIMESTAMP列:

1)此时如果TIMESTAMP列没有显示的指定not null属性,那么默认的该列可以为null,此时向该列中插入null值时,会直接记录null,而不是current timestamp。

2)不会自动的为表中的第一个TIMESTAMP列加上DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP属性,除非你在建表的时候显示的指明。

3)如果TIMESTAMP列被加上了not null属性,并且没有指定默认值。这时如果向表中插入记录,但是没有给该TIMESTAMP列指定值的时候,如果strict sql_mode被指定了,那么会直接报错。如果strict sql_mode没有被指定,那么会向该列中插入’0000-00-00 00:00:00’并且产生一个warning。

Dao层设计开发

创建数据库

-- 数据库初始化脚本
-- 创建数据库
CREATE DATABASE seckill;
-- 使用数据库
use seckill;
--创建秒杀库存表
CREATE TABLE seckill(
  `seckill_id` BIGINT NOT NUll AUTO_INCREMENT COMMENT '商品库存ID',
  `name` VARCHAR(120) NOT NULL COMMENT '商品名称',
  `number` int NOT NULL COMMENT '库存数量',
  `start_time` TIMESTAMP NOT NULL COMMENT '秒杀开始时间',
  `end_time` TIMESTAMP NOT NULL COMMENT '秒杀结束时间',
  `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (seckill_id),
  key idx_start_time(start_time),
  key idx_end_time(end_time),
  key idx_create_time(create_time)
)ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';

--初始化数据
INSERT INTO seckill(name,number,start_time,end_time)
VALUES
  ('1000元秒杀iphone6',100,'2018-05-01 00:00:00','2018-05-02 00:00:00'),
  ('800元秒杀ipad',200,'2018-05-01 00:00:00','2018-05-02 00:00:00'),
  ('6600元秒杀mac book pro',300,'2018-05-01 00:00:00','2018-05-02 00:00:00'),
  ('7000元秒杀iMac',400,'2018-05-01 00:00:00','2018-05-02 00:00:00');
-- 秒杀成功明细表
-- 用户登录认证相关信息(简化为手机号)
CREATE TABLE success_killed(
  `seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID',
  `user_phone` BIGINT NOT NULL COMMENT '用户手机号',
  `state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货',
  `create_time` TIMESTAMP NOT NULL COMMENT '创建时间',
  PRIMARY KEY(seckill_id,user_phone),/*联合主键*/
  KEY idx_create_time(create_time)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';

--连接数据库控制台
mysql -uroot -p
-- SHOW CREATE TABLE seckill;#显示表的创建信息

创建实体类

package org.seckill.entity;

import java.util.Date;

public class Seckill {
    private long seckillId;
    private String name;
    private int number;
    private Date startTime;
    private Date endTime;
    private Date createTime;

    public long getSeckillId() {
        return seckillId;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public Date getStartTime() {
        return startTime;
    }

    public void setStartTime(Date startTime) {
        this.startTime = startTime;
    }

    public Date getEndTime() {
        return endTime;
    }

    public void setEndTime(Date endTime) {
        this.endTime = endTime;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    @Override
    public String toString() {
        return "Seckill{" +
                "seckillId=" + seckillId +
                ", name='" + name + '\'' +
                ", number=" + number +
                ", startTime=" + startTime +
                ", endTime=" + endTime +
                ", createTime=" + createTime +
                '}';
    }
}
package org.seckill.entity;

import java.util.Date;

public class SuccessKilled {
    private long seckillId;

    private long userPhone;

    private short state;

    private Date createTime;

    private Seckill seckill;

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public long getUserPhone() {
        return userPhone;
    }

    public void setUserPhone(long userPhone) {
        this.userPhone = userPhone;
    }

    public short getState() {
        return state;
    }

    public void setState(short state) {
        this.state = state;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public long getSeckillId() {
        return seckillId;
    }

    public void setSeckill(Seckill seckill) {
        this.seckill = seckill;
    }

    @Override
    public String toString() {
        return "SuccessKilled{" +
                "seckillId=" + seckillId +
                ", userPhone=" + userPhone +
                ", state=" + state +
                ", createTime=" + createTime +
                '}';
    }
}

创建dao层的接口

package org.seckill.dao;

import org.seckill.entity.Seckill;

import java.util.Date;
import java.util.List;

public interface SeckillDao {
    /**
     * 减库存
     * @param seckillId
     * @param killTime
     * @return 如果影响行数>1,表示更新库存的记录行数
     */
    int reduceNumber(long seckillId, Date killTime);

    /**
     * 根据id查询秒杀的商品信息
     * @param seckillId
     * @return
     */
    Seckill queryById(long seckillId);

    /**
     * 根据偏移量查询秒杀商品列表
     * @param off
     * @param limit
     * @return
     */
    List queryAll(int off, int limit);
}
package org.seckill.dao;

import org.seckill.entity.SuccessKilled;

public interface SuccessKilledDao {
    /**
     * 插入购买明细,可过滤重复
     * @param seckillId
     * @param userPhone
     * @return插入的行数
     */
    int insertSuccessKilled(long seckillId,long userPhone);


    /**
     * 根据秒杀商品的id查询明细SuccessKilled对象(该对象携带了Seckill秒杀产品对象)
     * @param seckillId
     * @return
     */
    SuccessKilled queryByIdWithSeckill(long seckillId, long userPhone);
}

配置mybatis

在resources包下创建MyBatis全局配置文件mybatis-config.xml文件




    
    
        
        
        
        

        
        
    


创建mapperxml映射文件

在resources下创建mapper包,在包下创建对应Dao接口的xml映射文件。SeckillDao.xml和SuccessKilledDao.xml



    
    
        UPDATE seckill
        SET number = number-1
        WHERE seckill_id=#{seckillId}
        AND start_time  #{killTime}
        AND end_time >= #{killTime}
        AND number > 0;
    

    

    


spring配置文件



    
    

    
    
        
        

        
        
        
        

        
        
        
        
        

        
        
        
        
    

    
    
    
        
        
        
        
        
        
        
        
        
    

    
    
        
        
        
        
    

需要我们在resources包下创建jdbc.properties用于配置数据库的连接信息,内容如下:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false
jdbc.username=root
jdbc.password=newpass

利用junit4单元测试进行测试

首先测试SeckillDao.java,利用IDEA快捷键shift+command+T对SeckillDao.java进行测试,然后IDEA会自动在test包的java包下为我们生成对SeckillDao.java中所有方法的测试类SeckillDaoTest.java,内容如下:

public class SeckillDaoTest {
    @Test
    public void reduceNumber() throws Exception {

    }

    @Test
    public void queryById() throws Exception {

    }

    @Test
    public void queryAll() throws Exception {

    }
}

你可能感兴趣的:(ssm,java,ssm,秒杀)