基于SSM实现高并发秒杀Web项目(二)
这一部分是基于SSM实现高并发秒杀业务的第二节,主要介绍秒杀业务Service层的设计和实现,基于Spring托管Service实现类,并使用了Spring声明式事务。
源码可以在微信公众号【程序员修炼营】中获取哦,文末有福利
一、秒杀Service层接口设计
①准备工作:
新建service、dto、exception三个包。
dto:数据传输层,存放一些表示数据的类型。dto和entity较像,entity是业务(数据库表)的封装,dto是关注web和service之间的数据传递,此外dto层还包括了json格式的封装类,以便封装json数据。
exception:spring声明式事务只接收RuntimeException(运行期异常),只对运行期异常进行回滚。
②Service层接口设计
service/SeckillService.java 秒杀接口包含4个方法:查询全部秒杀记录、查询单个秒杀记录、暴露秒杀地址、执行秒杀。
配合接口中的方法,要同时设计好dto类以及异常类。
具体代码及注释如下:
service/SecKillService
public interface SecKillService {
//查询全部的秒杀记录
List<Seckill> getSeckillList();
//查询单个秒杀记录
Seckill getSecKillById(long seckillId);
//在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间,是为了防止秒杀开启前就通过接口提前进行秒杀
Exposer exporySecKillUrl(long seckillId);
//执行秒杀操作,有可能失败,有可能成功,所以要抛出我们允许的异常
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SecKillException, RepeatKillException, SecKillCloseException;
}
dto/Exposer
//暴露秒杀地址
public class Exposer {
private long seckillId;
//是否开启秒杀
private boolean exposed;
//md5加密方式
private String md5;
//系统当前时间(毫秒)
private long now;
//开启时间
private long start;
//结束时间
private long end;
}
后面自行补充相应的构造方法及getter()、setter()方法。
dto/SeckillExecution
//封装秒杀执行后的结果
public class SeckillExecution {
private long seckillId;
//秒杀执行结果状态
private int state;
//状态表示
private String stateInfo;
//秒杀成功对象
private SuccessKilled successKilled;
public SeckillExecution(long seckillId, SeckillStateEnum stateEnum, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = stateEnum.getState();
this.stateInfo = stateEnum.getStateInfo();
this.successKilled = successKilled;
}
public SeckillExecution(long seckillId, SeckillStateEnum stateEnum) {
this.seckillId = seckillId;
this.state = stateEnum.getState();
this.stateInfo = stateEnum.getStateInfo();
}
}
exception/SecKillException
//秒杀相关业务异常
public class SecKillException extends RuntimeException{
public SecKillException(String message){
super(message);
}
public SecKillException(String message, Throwable cause){
super(message, cause);
}
}
exception/RepeatKillException
//重复秒杀异常
public class RepeatKillException extends SecKillException{
public RepeatKillException(String message){
super(message);
}
public RepeatKillException(String message, Throwable cause){
super(message, cause);
}
}
exception/SecKillCloseException
//秒杀关闭异常
public class SecKillCloseException extends SecKillException{
public SecKillCloseException(String message){
super(message);
}
public SecKillCloseException(String message, Throwable cause){
super(message);
}
}
二、秒杀Service层接口实现
service包下新建接口实现类SecKillServiceImpl
public class SecKillServiceImpl implements SecKillService{
private Logger logger = LoggerFactory.getLogger(this.getClass());
private SeckillDao seckillDao;
private SuccessKillDao successKillDao;
//md5盐值字符串,用于混淆md5
private final String slat = "asfsafse345!$#&*#)(uiashgfdq12";
@Override
public List<Seckill> getSeckillList() {
return seckillDao.queryAll(0,4);
}
@Override
public Seckill getSecKillById(long seckillId) {
return seckillDao.queryById(seckillId);
}
@Override
public Exposer exportSecKillUrl(long seckillId) {
Seckill seckill = seckillDao.queryById(seckillId);
if (seckill == null){
return new Exposer(false, seckillId);
}
Date startTime = seckill.getStartTime();
Date endTime = seckill.getEndTime();
Date nowTime = new Date();//系统当前时间
if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()){
return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
}
//转化特定字符串的过程,不可逆
String md5 = getMD5(seckillId);
return new Exposer(true, md5, seckillId);
}
private String getMD5(long secKillId){
String base = secKillId + "/" + slat;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
@Override
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SecKillException, RepeatKillException, SecKillCloseException {
if (md5 == null || !md5.equals(getMD5(seckillId))){
throw new SecKillException("seckill data rewrite");
}
//执行秒杀逻辑:减库存+记录购买行为
Date nowTime = new Date();
try {
int updateCount = seckillDao.reduceNumber(seckillId, nowTime);//减库存
if (updateCount <= 0){//没有更新到记录,,秒杀结束
throw new SecKillCloseException("seckill is closed");
} else {
//记录购买行为
int insertCount = successKillDao.insertSuccessKilled(seckillId, userPhone);
if (insertCount <= 0){//重复秒杀了
throw new RepeatKillException("seckill repeated");
} else {//秒杀成功
SuccessKilled successKilled = successKillDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successKilled);
}
}
} catch (SecKillCloseException e1){
throw e1;
} catch (RepeatKillException e2){
throw e2;
}catch(Exception e){
logger.error(e.getMessage(), e);
//所有编译期异常转化为运行期异常,Spring声明式事务会进行rollback回滚
throw new SecKillException("seckill inner error:" + e.getMessage());
}
}
}
用常量表示数据字典,新建枚举类enums/SeckillStateEnum 存放状态,状态信息对应的常量。
//使用枚举表示常量数据字典
public enum SeckillStateEnum {
SUCCESS(1,"秒杀成功"),
END(0,"秒杀结束"),
REPEAT_KILL(-1,"重复秒杀"),
INNER_ERROR(-2,"系统异常"),
DATE_REWRITE(-3,"数据篡改");
private int state;
private String stateInfo;
SeckillStateEnum(int state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
}
public int getState() {
return state;
}
public String getStateInfo() {
return stateInfo;
}
public void setState(int state) {
this.state = state;
}
public void setStateInfo(String stateInfo) {
this.stateInfo = stateInfo;
}
public static SeckillStateEnum stateOf(int index) {
for (SeckillStateEnum state : values()) {
if (state.getState() == index)
return state;
}
return null;
}
}
三、基于Spring托管Service实现类
①Spring通过IOC来管理依赖,业务对象的简单依赖图如下:②使用SpringIOC有下面几点好处:
- 对象创建可以统一进行管理,无需new操作
- 规范的生命周期管理,init()、destory()等方法可以自由增加业务逻辑
- 灵活的依赖注入
- 一致的获取对象方式,可以很方便的从容器中取出对象
⑤进行依赖配置
在resources/spring 目录下新建spring-service.xml配置文件,以便向IOC装配service层的bean。
<?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.springframework.org/schema/tx"
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 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--扫描service包下所有使用注解的类型-->
<context:component-scan base-package="service"/>
</beans>
SecKillServiceImpl实现类上加上@Service注解,会将类自动转换为bean并注入到IOC容器中。
SeckillDao属性和SuccessKillDao属性上加上@Autowired注解,进行依赖注入。
四、使用Spring声明式事务
①Spring声明式事务有两种常见的用法:
- tx:advice+aop命名空间,一次配置永久生效。
- @Transcational,通过注解标注需要事务的方法。
②不是所有方法都需要事务,比如只有一条修改操作、只读操作就不需要事务控制,所以这里建议用注解控制(@Transcational)的方式标注需要事务的方法,而不建议采用tx-advice+aop命名空间对所有方法都标注事务。
④标注了@Transcational方法,开启一个事务,在最后return或者throw RuntimeException时才commit或rollback
⑤声明式事务配置
在spring-service.xml配置事务管理器,配置基于注解的声明式事务。
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置基于注解的声明式事务
默认使用注解来管理事务行为-->
<tx:annotation-driven transaction-manager="transactionManager"/>
⑥SecKillServiceImpl实现类中的executeSeckill方法上加上@Transactional注解,进行事务管理。
五、集成测试Service逻辑
①使用快捷键shift+ctrl+t快速生成SecKillServiceI接口的测试方法
② slf4j接口的实现类logback配置:在测试类中声明logger变量,然后新建resources/logback.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
③编写测试方法,逐一进行测试
//Spring启动时加载spring容器
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit spring的配置文件,完成bean的注入
@ContextConfiguration({"" + "classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"})
public class SecKillServiceTest {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private SecKillService secKillService;
@Test
public void getSeckillList() {
List<Seckill> seckills = secKillService.getSeckillList();
logger.info("list = {}", seckills);
}
@Test
public void getSecKillById() {
long id = 1000L;
Seckill seckill = secKillService.getSecKillById(id);
logger.info("seckill = {}", seckill);
}
//集成测试代码完整逻辑,注意可重复执行,注意测试的业务覆盖完整性。
@Test
public void seckillLogic() throws Exception {
long id = 1001L;
Exposer exposer = secKillService.exportSecKillUrl(id);
if (exposer.isExposed()) {
logger.info("exposer = {}", exposer);
long userPhone = 13054477731L;
String md5 = exposer.getMd5();
try {
SeckillExecution seckillExecution = secKillService.executeSeckill(id, userPhone, md5);
logger.info("result = {}", seckillExecution);
} catch (SecKillCloseException e) {
logger.error(e.getMessage());
} catch (RepeatKillException e) {
logger.error(e.getMessage());
}
}
else {
//秒杀未开启
logger.warn("exposer= {}", exposer);
}
}
@Test
public void executeSeckillProcedure() {
long seckillId = 1000L;
long phone = 1365422212L;
Exposer exposer = secKillService.exportSecKillUrl(seckillId);
if (exposer.isExposed()) {
String md5 = exposer.getMd5();
SeckillExecution execution = secKillService.executeSeckill(seckillId, phone, md5);
logger.info(execution.getStateInfo());
}
}
}
每个测试方法均验证通过,成功!