基于SSM实现高并发秒杀Web项目(一)

作者: 熊猫读书营 | 来源:发表于2018-05-29 20:37 被阅读28次

    整合运用SSM框架,做一个基于Spring+SpringMVC+Mybatis实现的高并发秒杀项目,后面采用redis缓存来优化高并发。学习秒杀类系统需求理解和实现以及解决高并发问题的常用技术。
    第一部分写一下业务分析已经DAO层的设计与实现。

    源码可以在微信公众号【程序员修炼营】中获取哦,文末有福利

    一、相关技术及开发环境

    ①Mysql:
    进行表设计
    常用SQL技巧
    事务和行级锁

    ②MyBatis:
    DAO层设计与开发
    Mybatis的合理使用
    Mybatis与Spring整合

    ③Spring:
    SpringIOC整合Service
    声明式事务的运用

    ④SpringMVC:
    Restful接口的设计与使用
    框架的运作流程
    Controller开发技巧

    ⑤前端:
    交互设计
    Bootstrap
    jQuery
    Ajax

    ⑥高并发
    高并发点和高并发分析
    优化思路并实现

    开发环境:Ubuntu16.04+IntelliJIDEA+jdk1.8+Mysql

    二、秒杀系统业务分析及难点分析

    ①秒杀系统的业务流程如下:


    可以从图中看到,秒杀系统的业务核心就是对库存的处理。

    ②用户针对库存的业务分析如下:


    因此要实现对库存的处理,关键就要记录秒杀成功的信息,即用户的购买行为。购买行为就是指 谁购买成功了;成功的时间/有效期;付款/发货信息。

    ③MySQL实现秒杀难点分析
    秒杀系统要解决的难点问题其实就是“竞争”的问题,即多个用户同时争夺物品。


    这个“竞争”反映到MySQL中就是事务加行级锁,在后面的实现中会逐一介绍。

    三、DAO层设计与开发

    首先使用IDEA构建一个Maven项目,引入相关的依赖,具体的依赖可以在pom.xml文件中查看。

    1)数据库设计

    ①创建秒杀库存表

    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 DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀开始时间',
      `end_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀结束时间',
      `create_time` TIMESTAMP  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
      ('6000元秒杀iphone8',100,'2018-05-20 00:00:00','2018-05-21 00:00:00'),
      ('3800元秒杀ipad',200,'2018-05-20 00:00:00','2018-05-21 00:00:00'),
      ('9600元秒杀mac book pro',300,'2018-05-20 00:00:00','2018-05-20 00:00:00'),
      ('9999元秒杀锤子TNT',400,'2018-05-20 00:00:00','2016-05-21 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='秒杀成功明细表';
    
    2)DAO实体及接口定义

    ①根据所创建的数据库表,定义出相应的实体类与数据查询接口。
    新建entity包,里面新建Seckill类和SuccessKilled类,属性与对应的数据表一致,getter()、setter()方法等。

    ②新建dao包,里面新建SeckillDao接口,定义秒杀商品所需要的方法

    public interface SeckillDao {
    
        //减库存
        //java没有保存形参的记录:有多个参数的时候会报错
        // 加上param注解 ,识别形参名称。
        int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);
    
        //根据id查询秒杀的商品信息
        Seckill queryById(long seckillId);
    
        //根据偏移量查询秒杀商品列表
        List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);
    
    }
    

    ③dao包下新建SuccessKillDao接口,定义秒杀成功所需要的方法

    public interface SuccessKillDao {
    
        //新增购买明细记录,由于表中设计了联合主键,可过滤重复
        int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
    
        //根据秒杀商品id查询明细SuccessKilled对象,该对象中携带了Seckill对象
        SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
    
    }
    
    3)基于Mybatis实现DAO接口

    Mybatis实现接口有两种方式:一是通过Mapper会自动实现DAO接口,二是可以通过API编程方式来实现DAO接口。
    我们这里采用第一种方式,减少代码量。

    ①resources目录下新建mybatis-config.xml文件,用于配置mybatis的相关属性。

    <?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)-->
            <setting name="mapUnderscoreToCamelCase" value="true"/>
        </settings>
    
    </configuration>
    

    ②resources目录下新建mapper目录,用于配置相关的映射文件。

    新建SeckillDao.xml如下:

    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <!--目的:为dao接口方法提供sql语句配置
       即针对dao接口中的方法编写对应的sql语句-->
    <mapper namespace="dao.SeckillDao">
        <update id="reduceNumber">
            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 * FROM seckill WHERE seckill_id = #{seckillId}
        </select>
    
        <select id="queryAll" resultType="Seckill">
            SELECT * FROM seckill
            ORDER BY create_time DESC
            limit #{offset},#{limit}
        </select>
        
    </mapper>
    

    新建SuccessKillDao.xml如下:

    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="dao.SuccessKillDao">
        <!-- insert ignore表示,如果中已经存在相同的记录,则忽略当前新数据;-->
        <!--当出现主键冲突时(即重复秒杀时),会报错;这里不想让程序报错,加入ignore,这样能通过返回的更新行数0 , 1 来确定是否更新成功-->
        <insert id="insertSuccessKilled">
            INSERT ignore INTO success_killed(seckill_id, user_phone, state)
            VALUES (#{seckillId},#{userPhone,0})
        </insert>
    
        <select id="queryByIdWithSeckill" resultType="SuccessKilled">
    
            <!--根据seckillId查询SuccessKilled对象,并携带Seckill对象-->
            <!--告诉mybatis把结果映射到SuccessKilled属性同时映射到Seckill属性-->
            SELECT
            sk.seckill_id,
            sk.user_phone,
            sk.create_time,
            sk.state,
            s.seckill_id as "seckill.seckill_id",
            s.name as "seckill.name",
            s.number as "seckill.number",
            s.start_time as "seckill.start_time",
            s.end_time as "seckill.end_time",
            s.create_time as "seckill.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>
    </mapper>
    
    4)Mybatis整合Spring

    ①resources目录下新建jdbc.properties文件,用于配置数据库信息

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/seckill
    jdbc.username=root
    jdbc.password=
    

    ②resources目录下新建spring 目录,里面新建spring-dao.xml文件,用于向Spring IOC中装配dao层所需要的bean,包括数据库连接池、Mybatis的SqlsessionFactory对象,以及扫描DAO接口实现类并注入进IOC

    <?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.xsd">
    
        <!--配置整合mybatis过程
        1.配置数据库相关参数-->
        <context:property-placeholder location="classpath:jdbc.properties"/>
    
        <!--2.数据库连接池-->
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <!--配置连接池属性-->
            <property name="driverClass" value="${jdbc.driver}" />
            <property name="jdbcUrl" value="${jdbc.url}" />
            <property name="user" value="${jdbc.username}" />
            <property name="password" value="${jdbc.password}" />
    
            <!--c3p0私有属性-->
            <property name="maxPoolSize" value="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.SqlSessionFactoryBean">
            <!--往下才是mybatis和spring真正整合的配置-->
            <!--注入数据库连接池-->
            <property name="dataSource" ref="dataSource"/>
            <!--配置mybatis全局配置文件:mybatis-config.xml-->
            <property name="configLocation" value="classpath:mybatis-config.xml"/>
            <!--扫描entity包,使用别名,多个用;隔开-->
            <property name="typeAliasesPackage" value="entity"/>
            <!--扫描sql配置文件:mapper需要的xml文件-->
            <property name="mapperLocations" value="classpath:mapper/*.xml"/>
        </bean>
    
        <!--4:配置扫描Dao接口包,动态实现DAO接口,注入到spring容器-->
        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
            <!--注入SqlSessionFactory、在数据库连接池加载后才注入SQlsessionfactory-->
            <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
            <!-- 给出需要扫描的Dao接口-->
            <property name="basePackage" value="dao"/>
        </bean>
    </beans>
    
    5)DAO层单元测试

    接下来就对SecKillDao和SuccessKillDao接口层进行测试。
    ①首先在SecKillDao类中使用idea的快捷键“shift+ctrl+t”快速生成测试类,注意选用jUnit4。
    编写测试代码如下:

    /**
     * 配置spring和junit整合,这样junit在启动时就会加载spring容器
     */
    @RunWith(SpringJUnit4ClassRunner.class)//Spring启动时加载spring容器
    @ContextConfiguration({"classpath:spring/spring-dao.xml"})//告诉junit spring的配置文件,完成bean的注入
    public class SeckillDaoTest {
    
        //注入Dao实现类依赖
        @Resource
        private SeckillDao seckillDao;
    
        @Test
        public void reduceNumber() {
            Date killTime = new Date();
            int updateCount = seckillDao.reduceNumber(1000L, killTime);
            System.out.println("updateCount" + updateCount);
        }
    
        @Test
        public void queryById() {
            long id = 1000;//数据库表中设置的从1000开始自增
            Seckill seckill = seckillDao.queryById(id);
            System.out.println(seckill);
        }
    
        @Test
        public void queryAll() {
            List<Seckill> seckills = seckillDao.queryAll(0,100);
            for (Seckill seckill : seckills){
                System.out.println(seckill);
            }
        }
    }
    

    逐个测试每个方法,均验证通过。

    ②同理验证SuccessKillDao接口,测试类如下:

    /**
     * 配置spring和junit整合,这样junit在启动时就会加载spring容器
     */
    @RunWith(SpringJUnit4ClassRunner.class)//Spring启动时加载spring容器
    @ContextConfiguration({"classpath:spring/spring-dao.xml"})//告诉junit spring的配置文件,完成bean的注入
    public class SuccessKillDaoTest {
    
        @Resource
        private SuccessKillDao successKillDao;
    
        @Test
        public void insertSuccessKilled() {
            Long secKillId = 1000L;
            long phone = 13066632317L;
            int insertCount = successKillDao.insertSuccessKilled(secKillId, phone);
            System.out.println(insertCount);
            //第一次的执行结果为1,秒杀成功;第二次执行结果为0,秒杀失败,因为表中设置了联合主键,不允许重复秒杀
        }
    
        @Test
        public void queryByIdWithSeckill() {
            Long secKillId = 1000L;
            long phone = 13066632317L;
            SuccessKilled successKilled = successKillDao.queryByIdWithSeckill(secKillId, phone);
            System.out.println(successKilled);
            System.out.println(successKilled.getSeckill());
        }
    }
    

    逐个测试每个方法,均验证通过。

    6)DAO层小结

    到此,秒杀项目的DAO层已经实现完成,由于采用了MybatisORM框架,DAO层工作演变为:接口设计+SQL编写。代码和SQL分离,方便后期review。
    并且DAO层只实现相关数据库的增删改查基本功能,具体的DAO拼接等复杂逻辑会在下一部分的Service层完成。

    相关文章

      网友评论

      本文标题:基于SSM实现高并发秒杀Web项目(一)

      本文链接:https://www.haomeiwen.com/subject/xuuodftx.html