慕课秒杀项目(基于SSM)
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"/>-->
<!-- <!– 关闭连接后不自动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">
<!--注入数据库连接池-->
<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>