慕课秒杀项目seckill

github代码

github代码:慕课网seckill

秒杀业务分析

添加/调整
秒杀/预售
付款/退货
发货/核账
商家
库存
用户

秒杀业务核心是对库存的处理。

用户针对库存业务分析:

减库存
完整事务
记录购买明细
数据落地

购买行为:记录秒杀成功的信息:购买成功;成功的时间/有效期付款/发货信息。
秒杀难点在于用户竞争。MySql处理方法为事务+行级锁
事务:start transaction;update 库存数量;insert 购买明细;commit。竞争出现在update减库存上。
行级锁:一个用户执行update,另外的用户等待。

秒杀功能:秒杀接口暴露;执行秒杀;相关查询。
代码开发阶段:DAO设计编码;Service设计编码;Web设计编码。

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 ='秒杀库存表';
-- Auto-increment 会在新记录插入表中时生成一个唯一的数字,默认开始值为1,每条新纪录递增1
-- 初始化数据
insert into seckill(name, number, start_time, end_time)
values ('1000元秒杀iphone6', '100', '2015-11-01 00:00:00', '2015-11-02 00:00:00'),
       ('500元秒杀ipad2', '200', '2015-11-01 00:00:00', '2015-11-02 00:00:00'),
       ('300元秒杀小米6', '300', '2015-11-01 00:00:00', '2015-11-02 00:00:00'),
       ('200元秒杀红米note', '400', '2015-11-01 00:00:00', '2015-11-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

DAO实体与接口编码

对照数据库建立Seckill实体类以及SuccessKilled实体类。
建立实体类的接口SeckillDao和SuccessKilledDao。

package dql.seckill.dao;


import dql.seckill.entity.Seckill;
import org.apache.ibatis.annotations.Param;

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

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);

    /**
     * 根据偏移量查询秒杀商品列表
     * xxMapper.xml中的#{}中的参数根据@Param括号中的参数来获取相应的数据
     *
     * @param offset
     * @param limit
     * @return
     */
    List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);

    /**
     * 使用存储过程执行秒杀
     *
     * @param paramMap
     */
    void killByProcedure(Map<String, Object> paramMap);
}

加入@Param(“offset”) 是因为java不保存形参的记录,所以需要mybatis提供的注解@Param标记实际的形参

package dql.seckill.dao;

import dql.seckill.entity.SuccessKilled;
import org.apache.ibatis.annotations.Param;

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


基于myBatis实现DAO

创建mybatis-config.xml和mapper映射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的getGeneratedKeys 获取数据库自增主键值-->
        <setting name="useGeneratedKeys" value="true"/>
        <!--使用列别名替换列名,默认:true-->
        <setting name="useColumnLabel" value="true"/>
        <!--        开启驼峰命名转换-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
</configuration>

映射的xml名与实体类名保持一致

<?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 namespace="dql.seckill.dao.SeckillDao">
    <!--目的:为DAO接口方法提供sql语句配置-->

    <update id="reduceNumber">
        <!-- 具体sql-->
        update
        seckill
        set number=number - 1
        where seckill_id = #{seckillId}
        and start_time <![CDATA[ <= ]]> #{killTime}
        and end_time >= #{killTime}
        and number
        > 0;
    </update>

    <select id="queryById" resultType="Seckill" parameterType="long">
        select seckill_id, name, number, start_time, end_time, create_time
        from seckill
        where seckill_id = #{seckillId}
    </select>

    <select id="queryAll" resultType="Seckill" parameterType="int">
        select seckill_id, name, number, start_time, end_time, create_time
        from seckill
        order by create_time desc
        limit #{offset},#{limit}
    </select>

    <!--mybatis调用存储过程-->
    <select id="killByProcedure" statementType="CALLABLE">
        call execute_seckill(
                #{seckillId,jdbcType=BIGINT,mode=IN},
                #{phone,jdbcType=BIGINT,mode=IN},
                #{killTime,jdbcType=TIMESTAMP,mode=IN},
                #{result,jdbcType=INTEGER,mode=OUT}
            )
    </select>
</mapper>
<?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 namespace="dql.seckill.dao.SuccessKilledDao">

    <insert id="insertSuccessKilled">
        <!-- 主键冲突,报错-->
        insert ignore into success_killed(seckill_id, user_phone,state)
        values (#{seckillId}, #{userPhone},0)
    </insert>

    <resultMap id="SuccessKilled" type="SuccessKilled">
        <id column="seckill_id" property="seckillId"/>
        <result column="user_phone" property="userPhone"/>
        <result column="create_tiem" property="createTime"/>
        <result column="state" property="state"/>
        <association property="seckill" javaType="Seckill">
            <id column="seckill_id" property="seckillId"/>
            <result column="name" property="name"/>
            <result column="number" property="number"/>
            <result column="start_time" property="startTime"/>
            <result column="end_time" property="endTime"/>
            <result column="create_time" property="createTime"/>
        </association>
    </resultMap>

    <select id="queryByIdWithSeckill" resultMap="SuccessKilled">
        <!--        根据id查询SuccessKilled并携带Seckill实体-->
        <!--        如何告诉MyBatis把结果映射到SuccessKilled同时映射seckill属性-->
        <!-- myBatis核心:可以自由控制Sql-->
        select sk.seckill_id,
        sk.user_phone,
        sk.create_time,
        sk.state,
        s.seckill_id,
        s.name,
        s.number,
        s.start_time,
        s.end_time,
        s.create_time
        from success_killed sk
        inner join seckill s on sk.seckill_id = s.seckill_id
        where sk.seckill_id = #{seckillId} and sk.user_phone=#{userPhone}
    </select>
      <!--       inner join(等值连接) 只返回两个表中联结字段相等的行-->
</mapper>

myBatis整合Spring编码

建立spring-dao.xml,配置整合mybatis。

<?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
       https://www.springframework.org/schema/context/spring-context.xsd">
    <!--    配置整合mybatis过程-->
    <!--    1:配置数据库相关参数properties的属性:${url}-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--2:数据库连接池 更换c3p0连接池为阿里Druid连接池就OK-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <!--    配置连接池属性-->
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="maxActive" value="${jdbc.max}"/>
        <!--    c3p0连接池的私有属性-->
        <!--        <property name="maxPoolSize" value="30"/>-->
        <!--        <property name="minPoolSize" value="10"/>-->
        <!--        &lt;!&ndash;        关闭连接后不自动commit&ndash;&gt;-->
        <!--        <property name="autoCommitOnClose" value="false"/>-->
        <!--        &lt;!&ndash;        获取连接超时时间&ndash;&gt;-->
        <!--        <property name="checkoutTimeout" value="1000"/>-->
        <!--        &lt;!&ndash;        当获取连接失败重试次数&ndash;&gt;-->
        <!--        <property name="acquireRetryAttempts" value="2"/>-->
    </bean>

    <!--    3:配置SqlSessionFactory对象-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--注入数据库连接池-->
        <property name="dataSource" ref="dataSource"/>
        <!--        配置Mybatis全局配置文件:mybatis-config.xml-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!--        扫描entity包 使用别名-->
        <property name="typeAliasesPackage" value="dql.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-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <!--        给出需要扫描Dao包-->
        <property name="basePackage" value="dql.seckill.dao"/>
    </bean>

    <!--    RedisDao-->
    <bean id="redisDao" class="dql.seckill.dao.cache.RedisDao">
        <constructor-arg index="0" value="localhost"/>
        <constructor-arg index="1" value="6379"/>
    </bean>
</beans>

这里视频中使用的c3p0连接池,本地项目运行出错,换成阿里druid连接池就ok。

Service层

接口

package dql.seckill.service;

import dql.seckill.dto.Exposer;
import dql.seckill.dto.SeckillExecution;
import dql.seckill.entity.Seckill;
import dql.seckill.exception.RepeatKillException;
import dql.seckill.exception.SeckillCloseException;
import dql.seckill.exception.SeckillException;

import java.util.List;

/**
 * 业务接口:站在“使用者”角度设计接口
 * 三个方面:方法定义粒度,参数,返回类型(return 类型/异常)
 */

public interface SeckillService {

    /**
     * 查询所有秒杀记录
     *
     * @return
     */
    List<Seckill> getSeckillList();

    /**
     * 查询单个秒杀记录
     *
     * @param seckillId
     * @return
     */
    Seckill getById(long seckillId);

    /**
     * 秒杀开启时输出秒杀接口地址
     * 否则输出系统时间和秒杀时间
     *
     * @param seckillId
     */
    Exposer exportSeckillUrl(long seckillId);

    /**
     * 执行秒杀操作by 存储过程
     *
     * @param seckillId
     * @param userPhone
     * @param md5
     */
    SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5)
            throws SeckillException, RepeatKillException, SeckillCloseException;


    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException, RepeatKillException, SeckillCloseException;
}

md5用来加密。

使用Spring托管Service依赖配置

<?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"
       xmlns:tx="http://www.alibaba.com/schema/stat"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.alibaba.com/schema/stat http://www.alibaba.com/schema/stat.xsd">

    <!--    扫描service包下所有使用注解的类型-->
    <context:component-scan base-package="dql.seckill.service"/>

    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--        注入数据库连接池-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--    配置基于注解的声明式事务
    默认使用注解来管理事务行为
    -->
    <tx:annotation-driven/>

</beans>

Web层

设计Restful接口

统一资源接口要求使用标准的HTTP方法对资源进行操作,所以URI只应该来表示资源的名称,而不应该包括资源的操作。通俗来说,URI不应该使用动作来描述。
例如,下面是一些不符合统一接口要求的URI:

GET /getUser/1
POST /createUser
PUT /updateUser/1
DELETE /deleteUser/1

/模块/资源/{标识}/集合1/…
/user/{uid}/friends ->好友列表

整合配置SpringMVC框架

首先配置web.xml
整合的顺序:Mybatis->spring->springMVC

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1"
         metadata-complete="true">

    <!--    修改servlet版本为3.1-->
    <!--    配置DispatcherServlet-->
    <servlet>
        <servlet-name>seckill-dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--        配置springMVC需要加载的配置文件
        spring-dao.xml,spring-service.xml,spring-web.xml
        Mybatis -> spring -> springMVC
        -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/spring-*.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>seckill-dispatcher</servlet-name>
        <!--        默认匹配所有的请求-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

配置spring-web.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:mvc="http://www.springframework.org/schema/mvc"
       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/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 配置SpringMVC   -->
    <!--    1:开启SpringMVC注解模式-->
    <!--    简化配置:
    (1)自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
    (2)提供一系列:数据绑定,数字和日期的format @NumberFormat,@DataTimeFormat,
    xml,json默认读写支持
    -->
    <mvc:annotation-driven/>

    <!--    2.servlet-mapping 映射路径:"/"-->
    <!--    静态资源默认servlet配置
    1:加入对静态资源的处理:js,gif,png
    2:允许使用"/"做整体映射
    -->
    <mvc:default-servlet-handler/>

    <!--    3.配置jsp 显示ViewResolver-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!--    4;扫描web相关的bean-->
    <context:component-scan base-package="dql.seckill.web"/>

</beans>

版权声明:本文为weixin_48669196原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。