Java高并发秒杀业务Api-Service层构建过程
2018-05-05 本文已影响38人
markfork
章节目录
- 秒杀Service 接口开发工作
- 秒杀业务逻辑编写
- spring-IOC 管理 service 组件
- context:component-scan
- Spring 声明式事务
- junit测试
创建基本的代码包层
1.创建DTO - 数据传输层对象
网络数据到达Controller 层后会使用框架自带的数据绑定 以及反序列化为dto对
象,并作为参数传递至service层进行处理。
2.业务接口实现
注意:业务接口的实现需要站在使用者的角度去设计接口
- 方法定义粒度-非常明确,参数简练,直接,不要一个大map对象去传递,return 还可以抛出异常。
代码如下:
业务逻辑接口声明类 SecKillService.java
package org.seckill.service;
import org.seckill.domain.SecKill;
import org.seckill.dto.Exposer;
import org.seckill.dto.SecKillExcution;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SecKillCloseException;
import org.seckill.exception.SecKillException;
import java.util.List;
/**
* 业务接口的实现需要站在使用者的角度去设计接口
* 三个方面:方法定义粒度、参数、返回类型 dto就可以
*/
public interface SecKillService {
/**
* 返回秒杀商品列表
*
* @return
*/
List<SecKill> getSecKillList();
/**
* 查询秒杀商品单条记录
*
* @param secKillId
* @return
*/
SecKill getSecKillById(long secKillId);
/**
* 秒杀开启时,输出秒杀接口的地址,否则输出系统时间,和秒杀时间
*
* @param secKillId
* @return
*/
Exposer exportSecKillUrl(long secKillId);
/**
* 执行秒杀操作
* 验证当前的excuteSecKill id 与 传递过来的md5是否相同
*
* @param secKillId
* @param userPhone
* @param md5
*/
SecKillExcution excuteSecKill(long secKillId, String userPhone, String md5)
throws SecKillException, RepeatKillException, SecKillCloseException;
}
业务逻辑接口实现类-SecKillServiceImpl
package org.seckill.service.impl;
import org.seckill.dao.SecKillDao;
import org.seckill.dao.SuccessKilledDao;
import org.seckill.domain.SecKill;
import org.seckill.domain.SuccessKilled;
import org.seckill.dto.Exposer;
import org.seckill.dto.SecKillExcution;
import org.seckill.enums.SecKillStateEnum;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SecKillCloseException;
import org.seckill.exception.SecKillException;
import org.seckill.service.SecKillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.DigestUtils;
import java.util.Date;
import java.util.List;
@Service
public class SecKillServiceImpl implements SecKillService {
//在业务逻辑层打日志
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowire
private SecKillDao secKillDao;
@Autowire
private SuccessKilledDao successKilledDao;
//md5加盐,混淆加密
private final String salt = "asdasd8Zy*&ZCY87ywer7t678tzt67wer";
public List<SecKill> getSecKillList() {
return secKillDao.queryAll(0, 4);
}
public SecKill getSecKillById(long secKillId) {
return secKillDao.queryById(secKillId);
}
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 = null;
return new Exposer(secKillId, md5, true);
}
/**
* 生成对应秒杀商品的md5值,做参数校验
* 保证可重用
*
* @param secKillId
* @return
*/
private String getMD5(long secKillId) {
String base = secKillId + "/" + salt;//用户不知道salt
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
/**
* 执行秒杀
*
* @param secKillId
* @param userPhone
* @param md5
* @return
* @throws SecKillException
* @throws RepeatKillException
* @throws SecKillCloseException
*/
public SecKillExcution excuteSecKill(long secKillId, String userPhone, String md5) throws SecKillException, RepeatKillException, SecKillCloseException {
try {
//1.用户输入的md5值验证
if (md5 == null || !md5.equals(getMD5(secKillId))) {
throw new SecKillException("seckill data rewrite");
}
//2.减库存、执行秒杀逻辑+记录购买行为,执行秒杀时间
Date nowTime = new Date();
int updateCount = secKillDao.reduceStock(secKillId, nowTime);
if (updateCount <= 0) {
//没有更新到记录,秒杀结束
throw new SecKillCloseException("seckill is closed");
} else {
//3.记录购买行为 insertSuccessKilled 可能出现数据库连接超时等问题,所以需要外层try
int insertCount = successKilledDao.insertSuccessKilled(secKillId, userPhone);
//唯一验证,secKillId + userPhone
if (insertCount <= 0) {//数据库联合主键冲突
throw new RepeatKillException("seckill repeated");
} else {//秒杀成功,返回秒杀成功的实体
SuccessKilled successKilled = successKilledDao.queryByIdWithSecKill(secKillId, userPhone);
//不优雅的实现方式,在多处需要用到提示信息时,我们可以采用统一的常量去返回,这样待客户端提示语改变时,我们可以统一进行更改。
// return new SecKillExcution(secKillId, 1, "秒杀成功", successKilled);
return new SecKillExcution(secKillId, SecKillStateEnum.SUCCESS, successKilled);
}
}
} catch (SecKillCloseException e1) {
throw e1;//还是要回滚
} catch (RepeatKillException e2) {
throw e2;//还是要回滚
} catch (Exception e) {
logger.error(e.getMessage());
//所有异常最终会转化为 运行时异常,spring 的声明式事务会帮我们做rollback。
throw new SecKillException("seckill inner error" + e.getMessage());
}
}
}
数据传输层类- Exposer
package org.seckill.dto;
/**
* 暴露秒杀接口
*/
public class Exposer {
private boolean exposed;//秒杀是否开启标志位
private String md5; //加密措施,暴露地址包括一个md5值
private long now; //系统当前时间(毫秒),方便浏览器计算距离服务器秒杀开启时间
private long secKillId; //秒杀商品的id
private long start;
private long end;
//秒杀正在进行
public Exposer(long secKillId, String md5, boolean exposed) {
this.secKillId = secKillId;
this.md5 = md5;
this.exposed = exposed;
}
//秒杀结束或还没开启
public Exposer(boolean exposed, long secKillId, long now, long start, long end) {
this.exposed = exposed;
this.secKillId = secKillId;
this.now = now;
this.start = start;
this.end = end;
}
//没有相关商品秒杀活动
public Exposer(boolean exposed, long secKillId) {
this.exposed = exposed;
this.secKillId = secKillId;
}
public boolean isExposed() {
return exposed;
}
public void setExposed(boolean exposed) {
this.exposed = exposed;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public long getNow() {
return now;
}
public void setNow(long now) {
this.now = now;
}
public long getStart() {
return start;
}
public void setStart(long start) {
this.start = start;
}
public long getEnd() {
return end;
}
public void setEnd(long end) {
this.end = end;
}
public long getSecKillId() {
return secKillId;
}
public void setSecKillId(long secKillId) {
this.secKillId = secKillId;
}
@Override
public String toString() {
return "Exposer{" +
"exposed=" + exposed +
", md5='" + md5 + '\'' +
", now=" + now +
", secKillId=" + secKillId +
", start=" + start +
", end=" + end +
'}';
}
}
数据传输层-SecKillExcution
package org.seckill.dto;
import org.seckill.domain.SuccessKilled;
import org.seckill.enums.SecKillStateEnum;
/**
* 执行秒杀之后的结果
*/
public class SecKillExcution {
private long secKillId;
private int state;//状态的标识
private String stateInfo;//状态表示
private SuccessKilled successSecKilled;//秒杀成功对象
//成功 jakson 在转化枚举的时候会出现问题,不支持枚举序列化
public SecKillExcution(long secKillId, SecKillStateEnum secKillStateEnum, SuccessKilled successSecKilled) {
this.secKillId = secKillId;
this.state = secKillStateEnum.getState();
this.stateInfo = secKillStateEnum.getStateInfo();
this.successSecKilled = successSecKilled;
}
//失败,使用到枚举
public SecKillExcution(long secKillId, SecKillStateEnum secKillStateEnum) {
this.secKillId = secKillId;
this.state = secKillStateEnum.getState();
this.stateInfo = secKillStateEnum.getStateInfo();
}
public long getSecKillId() {
return secKillId;
}
public void setSecKillId(long secKillId) {
this.secKillId = secKillId;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getStateInfo() {
return stateInfo;
}
public void setStateInfo(String stateInfo) {
this.stateInfo = stateInfo;
}
public SuccessKilled getSuccessSecKilled() {
return successSecKilled;
}
public void setSuccessSecKilled(SuccessKilled successSecKilled) {
this.successSecKilled = successSecKilled;
}
@Override
public String toString() {
return "SecKillExcution{" +
"secKillId=" + secKillId +
", state=" + state +
", stateInfo='" + stateInfo + '\'' +
", successSecKilled=" + successSecKilled +
'}';
}
}
业务运行时异常类-SecKillException、RepeatKillException、SecKillCloseException
package org.seckill.exception;
/**
* 秒杀相关业务异常
*/
public class SecKillException extends RuntimeException {
public SecKillException(String message) {
super(message);
}
public SecKillException(String message, Throwable cause) {
super(message, cause);
}
}
package org.seckill.exception;
/**
* 重复秒杀的异常(运行时异常)
* 声明式事务,只接收运行时异常
*/
public class RepeatKillException extends SecKillException {
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
package org.seckill.exception;
/**
* 秒杀关闭异常-友好响应给用户
*/
public class SecKillCloseException extends SecKillException {
public SecKillCloseException(String message) {
super(message);
}
public SecKillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
2.spring-IOC管理 service 组件
spring-service.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" 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="org.seckill.service" />
<!-- 配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据库连接池-->
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 配置基于注解的声明式事务
默认使用注解来管理事务行为
-->
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
Spring 声明式事务
什么是声明式事务
底层原理:采用动态代理的方式为我们的事务核心逻辑添加开启事务、回滚提交事务的控制操作
声明式事务使用方式
推荐使用@Transactional
声明式事务的传播行为
定义:即事务方法的嵌套
一个业务需要调用多个声明了事务控制的方法,那么最新组合的事务是重新启动一个事务,还是说沿用老的事务呢?
默认的事务传播行为:
propagation_required
浅析如下:
ServiceA {
void methodA() {
ServiceB.methodB();
}
}
ServiceB {
void methodB() {
}
}
比如说ServiceB.methodB 事务传播行为定义为PROPAGATION_REQUIRED,
- 那么当ServiceA.methodA 调用 ServiceB.methodB 时,methodA起了新的事
务,那么ServiceB.methodB看到自己已经运行在ServiceA.methodA的事务内
部,就不再起新的事务。 - 而假如ServiceA.methodA运行的时候发现自己没有在事务中,他就会为自己
分配一个事务。这样,在ServiceA.methodA或者在ServiceB.methodB内的任何
地方出现异常,事务都会被回滚。即使ServiceB.methodB的事务已经被提交,
但是ServiceA.methodA在接下来fail要回滚,ServiceB.methodB也要回滚
相当于 methodB 通过自己的事务传播行为告诉methodA 自己使用事务的原
则,告诉methodA 你要有事务我methodB就用你的,如果methodA没有事务,
那你methodA就需要创建一个事务。
什么时候回滚事务
当业务方法抛出运行时异常(RuntimeException)的时候spring 事务管理器会进行commit
配置事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource">
</bean>
配置基于注解的声明式事务
<tx:annotation-driven transaction-manager="transactionManager"/>
使用注解控制事务方法的优点
- 开发团队达成一致约定,约定明确标注事务方法的编程风格
- 保证事务方法的执行时间尽可能的短,不要穿插其他的网络操作,RPC/HTTP请求,如果必须需要的话,那么将这些请求剥离出来,形成一个干净的方法调用。不要混合编写和外部系统进行网络通信的代码。
- 不是所有的方法都需要事务,select、insert操作,单条语句的insert、update都不需要事务操作、不需要并发控制&多个操作联合形成一个事务时,不需要设置事务,因为mysql有autocommit=1的设置。
上述是性能杀手啊 ,注意再注意。
单元测试
package org.seckill.service;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.domain.SecKill;
import org.seckill.dto.Exposer;
import org.seckill.dto.SecKillExcution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
@RunWith(SpringJUnit4ClassRunner.class)
@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() throws Exception {
List<SecKill> secKillList = secKillService.getSecKillList();
logger.info("list{}", secKillList);
}
@Test
public void getSecKillById() throws Exception {
long secKillId = 1000L;
SecKill secKill = secKillService.getSecKillById(secKillId);
logger.info("seckill{}", secKill);
}
@Test
public void exportSecKillUrl() throws Exception {
long secKillId = 1000L;
Exposer exposer = secKillService.exportSecKillUrl(secKillId);
logger.info("exposer{}", exposer);
}
@Test
public void excuteSecKill() throws Exception {
long secKillId = 1000L;
String userPhone = "15300815981";
String md5 = "6e3cc65f3b42e656bdbc55a6a381f5d0";
SecKillExcution secKillExcution = secKillService.excuteSecKill(secKillId, userPhone, md5);
logger.info("secKillExcution{}", secKillExcution);
}
}
intellj idea 下单元测试快捷键:ctrl+shift+t