二、高并发秒杀API之Dao层设计与实现

来源:互联网 发布:java smb 木马 编辑:程序博客网 时间:2024/06/16 06:11

源码参照:并发系统源码

 

1 业务分析

此部分见:业务分析


2 创建Maven项目        

      

       该秒杀系统是一个基于Maven管理的项目,一般用到的资源建议从官网获取,会更加全面和权威,避免过时和错误。

首先,用Eclipse创建Maven项目,详细创建过程见链接:Maven项目手动创建和自动创建

(1)完善目录结构创建文件夹,得到如下目录结构。

                   


(2)web.xml文件改变servlet版本,相关问题见

(3)pom.xml文件引入依赖 

 

3 数据库设计与实现

数据库构建主要是两个表的创建:秒杀表、明细表。这里主要使用手写DDL(Data Definition Language)来创建,

--记录每次上线的DDL修改--上线V1.1ALTER TABLE seckillDROP INDEX idx_create_time,ADD index idx_c_s(start_time,create_time);--上线v1.2--ddl --数据库初始化脚本 --创建数据库CREATE databaseseckill;--使用数据库useseckill;--创建秒杀库存表,只有InnoDB数据库支持事务CREATE TABLE seckill(seckill_idBIGINT NOT NULL AUTO_INCREMENT COMMENT '商品库存id',name VARCHAR(120) NOT NULL COMMENT '商品名称',numberint NOT NULL COMMENT '库存数量',/*默认时间戳要在自定义的时间前面,否则会出错*/create_timeTIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP  COMMENT '创建时间',start_timeTIMESTAMP NOT NULL COMMENT '秒杀开启时间',end_timeTIMESTAMP NOT NULL COMMENT '秒杀结束时间',PRIMARY KEY (seckill_id),keyidx_start_time(start_time),keyidx_end_time(end_time),keyidx_create_time(create_time))ENGINE=InnoDBAUTO_INCREMENT=1000 DEFAULTCHARSET=utf8 COMMENT='秒杀库存表'; --初始化数据insert into seckill(name,number,start_time,end_time)values('1000元秒杀iphone6',100,'2016-07-07 00:00:00','2016-07-08 00:00:00'),values('500元秒杀ipad2',200,'2016-07-07 00:00:00','2016-07-08 00:00:00'),values('400元秒杀小米note4',300,'2016-07-07 00:00:00','2016-07-08 00:00:00'),values('200元秒杀红米note',300,'2016-07-07 00:00:00','2016-07-08 00:00:00'); --秒杀成功明细表--用户登录认证相关信息create table success_killed(seckill_idBIGINT not null COMMENT '秒杀商品id',user_phoneBIGINT not null COMMENT '用户手机号',statetinyint not null DEFAULT -1 COMMENT '状态标识:-1:无效  0:成功 1:已付款',create_timetimestamp not null COMMENT '创建时间',PRIMARY key(seckill_id,user_phone),/*联合主键*/keyidx_create_time(create_time))ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';

 

 

4 Dao层设计开发


4.1 Entity实体和Dao接口编码

     创建完数据库表

表(table)----对应---》实体(Entity), 表中的列(column)----对应---》实体中的属性(property)

     同时也可以变通一下:用多对一的关系。


4.1.1Entity

创建实体类Seckill.java

public class Seckill {    private  long seckillId;    private String name;    private int number;    private Date createTime;    private Date startTime;    private Date endTime;    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 getCreateTime() {        return createTime;    }    public void setCreateTime(Date createTime) {        this.createTime = createTime;    }    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;    }    @Override    public String toString() {        return "Seckill [seckillId=" + seckillId + ", name=" + name                + ", number=" + number + ", createTime=" + createTime                + ", startTime=" + startTime + ", endTime=" + endTime + "]";    }   }


 

SuccessKilledDao.java

public class SuccessKilled {    private long seckilled;    private long userPhone;    private short state;    private Date createTime;       //多对一的复合属性    private Seckill seckill;       public long getSeckilled() {        return seckilled;    }    public void setSeckilled(long seckilled) {        this.seckilled = seckilled;    }    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 Seckill getSeckill() {        return seckill;    }    public void setSeckill(Seckill seckill) {        this.seckill = seckill;    }    @Override    public String toString() {        return "SuccessKilled [seckilled=" + seckilled + ", userPhone="                + userPhone + ", state=" + state + ", createTime=" + createTime                + ", seckill=" + seckill + "]";    }}

 

4.1.2DAO

SeckillDao.java

public interface SeckillDao {    /**     * 减库存     * @param seckillId     * @param killTime     * @return如果影响行数>1,表示更新行数     */    int reduceNumber(@Param("seckillId") long seckillId,@Param("killTime") Date killTime);       /**     * 根据id查询秒杀库存     * @param seckillId     * @return插入的行数     */    Seckill queryById(long seckillId);       /**     * 根据偏移量查询秒杀列表     * @param offset     * @param limit     * @return     * 唯一形参自动赋值     * 当有多个参数的时候要指定实际的形参名称赋值,不然找不到对应值,因为Java并没有保存形参的记录     *java在运行的时候会把List<Seckill> queryAll(int offset,intlimit);中的参数变成这样:queryAll(int arg0,int arg1),这样我们就没有办法去传递多个参数     */    List<Seckill> queryAll(@Param("offset")int offset,@Param("limit")int limit);}

 

SuccessKilledDao.java

public interface SuccessKilledDao {    /**     * 插入购买明细,过滤重复(联合唯一主键)     * @param seckillId     * @param userPhone     * @return     */    int insertSuccessKilled(@Param("seckillId") long seckillId,@Param("userPhone") long userPhone);    /**     * 根据Id查询SuccessKilled并携带秒杀产品对象实体。     * @param seckillId     * @return     */    SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId,@Param("userPhone") long userPhone);}

 

4.2 基于Mybatis的Dao层实现

      设计好了Database和Dao层,剩下的就是需要一个映射关系来链接两者,将数据和对象对应起来,这时就需要Mybatis或者Hibernate来完成任务,“关系型对象映射--- ORM(Object/RelationshipMapping)”的名称也是由此而来。

                    


Mybatis特点

    参数需要提供+SQL需要自己写(比较灵活)=Entity/List;

    SQL文件可以写在xml文件或注解当中,实际使用当中提倡使用xml配置文件来写SQL,避免修改重新编译类等。

如何实现DAO接口?

    第一种方式:通过Mapper自动实现Dao层接口(推荐使用),将执行结果集封装成我们想要的方式;

    第二种方式:通过API编程的方式实现Dao接口。

 

Xml配置文件:

     参照官网相关标准配置:点击打开链接


4.2.1Mybatis总配置文件

配置文件 mybatis-config.xml

<?xml version="1.0"encoding="UTF-8" ?><!DOCTYPE configuration        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"        "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration>    <!--配置全局属性-->    <settings>        <!--使用jdbc的getGeneratekeys获取自增主键值-->        <setting name="useGeneratedKeys" value="true"/>        <!--使用列别名替换列名  默认值为true        select name as title(实体中的属性名是title) form table;        开启后mybatis会自动帮我们把表中name的值赋到对应实体的title属性中        -->        <setting name="useColumnLabel" value="true"/>         <!--开启驼峰命名转换Table:create_time到 Entity(createTime)-->        <!-- 驼峰命名规范:Mybatis会自动转化列属性 -->        <setting name="mapUnderscoreToCamelCase" value="true"/>    </settings> </configuration>

 

<!-- mybatis官方文档的入门-全局配置-mybatis的标准配置 -->

 

4.2.2Dao配置文件

保证Dao配置文件和Dao层类名相同,形成一种规范。

SeckillDao.xml文件,为Dao层定义的接口提供sql语句,Dao层的接口中的参数可以自动被Mybatis绑定到配置文件对应的Sql语句里面的参数,文件内容如下:

<?xml version="1.0"encoding="UTF-8" ?><!DOCTYPE mapper    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"    "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper>    <!-- 目的:为Dao层接口方法提供sql语句配置 -->    <!-- 为第一个方法提供sql语句 -->    <update id="reduceNumber">        <!-- 具体sql,返回影响的行数 -->        update seckill set number=number-1        where seckill_id=#{seckill_Id}        and start_time <![CDATA[<=]]> #{killTime}        and end_time<![CDATA[>=]]> #{killTime}        and number>0;              </update>       <select id="queryById"resultType="Seckill" parameterType="long">        selectseckill_id,name,number,create_time,start_time,end_time        from seckill        where seckill_id=#{seckillId}          </select>       <select id="queryAll"resultType="Seckill">        selectseckill_id,name,number,create_time,start_time,end_time        from seckill        order by create_time desc        limit #{offset},#{limit} <!-- 在偏移量之后取的行数 -->        </select>  </mapper>

     

配置文件里面的SQL 语句和Dao层的接口定义的方法一一对应,SQL里面用到的属性和数据库中属性名相同,SQL语句里面的参数名和接口定义方法的形参保持相同。

 

SuccessSeckilledDao.xml配置

<!DOCTYPE mapper    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"    "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="org.seckill.dao.SuccessKilledDao">    <insert id="insertSuccessKilled">    <!-- 使用ignore在主键冲突的时候报错返回0,避免插入数值 -->        insert ignore intosuccess_killed(seckill_id,user_phone,state)        values(#{seckillId},#{userPhone},0)        </insert>       <select id="queryByIdWithSeckill"resultType="SuccessKilled">        <!-- 根据Id查询SuccessKilled并携带秒杀产品对象实体。 -->        <!-- 如何告诉Mybatis把结果映射到SuccessKilled同时映射seckill属性 -->        <!--级联处理:通过别名告诉Mybatis怎么映射到seckill实体里面的属性 -->        select            sk.seckill_id,            sk.user_phone,            sk.create_time,            sk.state,            s.seckill_id"seckill.seckill_id",            s.name "seckill.name",            s.number "seckill.number",            s.start_time"seckill.start_time",            s.end_time"seckill.end_time",            s.create_time"seckill.create_time"        from success_killed sk        inner join seckill s onsk.seckill_id=s.seckill_id        where sk.seckill_id=#{seckillId} andsk.user_phone=#{userPhone}    </select></mapper> 

 

4.2.3整合Mybatis+Spring

将两者整合是为了更好的优化代码,整合目标是为了更少的编码更少的配置足够的灵活

首先,更少的编码:

使用Mybatis只用写接口,不用写接口的实现方法,Mybatis会自动帮忙实现接口。

看下面的一个接口与SQL语句的对应关系:

                                  

         这样可以更少的编码;

其次,更少的配置:

     当有很多Dao的映射文件的时候,则需要配置很多的<mapperresource=’”mapper/SeckillDao.xml”/>……,这样会比较麻烦,对于开发人员来说是一个很重的配置维护成本,这时候使用Mybatis可以自动扫描某一个目录下的所有以xml结尾的文件,这样就有了更少的配置。

     另外,当有很多Dao的时候,需要配置对应的 <bean id=”” class=””/>,使用mybatis则不用配置这些东西,因为mybatis可以自动实现Dao接口,自动注入Spring容器。

    这体现出了使用Mybatis可以更少的对文件进行配置。

最后,足够的灵活:

      使用Mybatis就是因为可以自由定制SQL并且传参,结果可以实现自动赋值,这也是为什么Mybatis为什么如此受欢迎的原因,整合了Spring的Mybatis仍然保持了足够的灵活性,当我们使用这两个技巧的时候:XML提供SQL语句和DAO接口提供Mapper,可以在Spring整合后实现,这也是我们在开发中经常推荐的方式。

 

整合编码

Spring和Mybatis整合的总体配置:(其中spring配置在Spring的官方文档里面有介绍

Spring-dao.xml:

<?xml version="1.0"encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xmlns:context="http://www.springframework.org/schema/context"        xsi:schemaLocation="http://www.springframework.org/schema/beans         http://www.springframework.org/schema/beans/spring-beans.xsd         http://www.springframework.org/schema/context         http://www.springframework.org/schema/context/spring-context-3.0.xsd" >                  <!-- 配置整合Mybatis的过程 -->         <!-- 1:配置数据库相关参数  properties的属性:${url}         -->         <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 加载配置参数的文件所在地 -->                  <!-- 2:数据库连接池 -->         <bean id="dataSource"class="com.mchange.v2.c3p0.ComboPooledDataSource">                <!-- 配置连接池属性 -->                <!-- c3p0连接池的基本属性 -->                <property name="driverClass"value="${driver}"/>                <property name="jdbcUrl"value="${url}"/>                <property name="user"value="${username}"/>                <property name="password"value="${password}"/>                               <!--连接池的私有属性根据高并发应用场景 -->                <property name="maxPoolSize"value="30"/><!--连接池最多保留30个对象 -->                <property name="minPoolSize"value="10"/>                               <!-- 关闭连接后不自动commit -->                <property name="autoCommitOnClose"value="false"/>                <!-- 获取连接超时时间 -->                <property name="checkoutTimeout"value="1000"/>                <!-- 当前获取连接失败重试次数 -->                <property name="acquireRetryAttempts"value="2"/>                      </bean>                           <!-- 使用框架趋势:约定大于配置,将相应的文件放在对应包下,通过已配置项可以自动扫描 -->         <!-- 3:配置 SqlSessionFactory对象  真正的整合配置-->         <bean id="sqlSessionFactory"class="org.mybatis.spring.SqlSessionFactory">            <!--注入数据库连接池  -->            <property name="dataSource"ref="dataSource"/>            <!-- 配置Mybatis全局配置文件:mybatis-config.xml -->            <property name="configLocation"value="classpath:mybatis-config.xml"/>            <!-- 扫描entity包 使用别名org.seckill.entity -->            <property name="typeAliasesPackage"value="org.seckill.entity"/>            <!-- 扫描Sql配置文件:mapper需要的xml文件 -->            <property name="mapperLocations"value="classpath:mapper/*.xml"/>         </bean>               <!-- 4: 配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中-->        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">           <!--注入SqlSessionFactory 使用sqlSessionFactoryBeanName可以在用的时候再找sqlSessionFactory,防止提前初始化-->           <property name="sqlSessionFactoryBeanName"value="sqlSessionFactory"/>           <!-- 给出需要扫描Dao接口包-->           <property name="basePackage"value="org.seckill.dao"/>    </bean> </beans>

 

 

4.3 Junit单元测试

       

        编写两个Dao类接口的测试类,这里使用Junit4来进行单元测试;


4.3.1SeckillDaoTest

/** * 配置Spring和Junit整合,Junit启动时加载SPringIOC容器 * spring-test,junit:spring测试的依赖 * 1:RunWith:Junit本身需要的依赖 * @author Terence * */@RunWith(SpringJUnit4ClassRunner.class)//2:告诉Junit Spring的配置文件@ContextConfiguration({"classpath:spring/spring-dao.xml"})public class SeckillDaoTest {    //3:注入Dao实现类依赖 --会自动去Spring容器中查找seckillDao的实现类注入到单元测试类    @Resource    private SeckillDao seckillDao;       @Test    public void testQueryById() throws Exception {    long id=1000;    Seckillseckill=seckillDao.queryById(id);    System.out.println(seckill.getName());    System.out.println(seckill);    /*     * 1000元秒杀iphone6     * Seckill [seckillId=1000, name=1000元秒杀iphone6, number=100, createTime=Thu Aug 03 09:20:36 CST2017, startTime=Thu Jul 07 00:00:00 CST 2016, endTime=Fri Jul08 00:00:00 CST 2016]     */    }     /**     * java在运行的时候会把List<Seckill> queryAll(int offset,intlimit);中的参数变成这样:queryAll(int arg0,int arg1),这样我们就没有办法去传递多个参数     *     * @throws Exception     */    @Test    public void queryAll() throws Exception {    List<Seckill> seckills=seckillDao.queryAll(0, 10);    for(Seckill s:seckills)    {         System.out.println(s);    }    }       @Test    public void reduceNumber() throws Exception {    long seckillId=1000;    Date killTime=new Date();    int updateCount=seckillDao.reduceNumber(seckillId, killTime);    System.out.println("updateCount="+updateCount);     }}

 

4.3.2SuccessKilledTest

@RunWith(SpringJUnit4ClassRunner.class)//2:告诉Junit Spring的配置文件@ContextConfiguration({"classpath:spring/spring-dao.xml"})public class SuccessKilledTest {       @Resource    private SuccessKilledDao successKilledDao;        /*     *inserCount=0:已经插入相同记录     *inserCount=1:当前执行操作插入了一条记录     */    @Test    public void insertSuccessKilledTest() throws Exception   {         long seckillId=1002;        long userPhone=13475191898L;        int insertCount=successKilledDao.insertSuccessKilled(seckillId,userPhone);        System.out.println("insertCount="+insertCount);    }       @Test    public void queryByIdWithSeckill() throws Exception    {        long seckillId=1002;        long userPhone=13475191898L;        SuccessKilled successKilled=successKilledDao.queryByIdWithSeckill(seckillId,userPhone);        System.out.println(successKilled);        System.out.println(successKilled.getSeckill());    }}

        

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         

阅读全文
0 0