整合SSM框架的高并发和商品秒杀项目(二)-Java高并发秒杀API之Service层
(二)Java高并发秒杀API之Service层
1.理清思路
首先在编写Service
层代码前,我们应该首先要知道这一层到底时干什么的,这里摘取来自ITEYE
一位博主的原话
Service层主要负责业务模块的逻辑应用设计。同样是首先设计接口,再设计其实现的类,接着再Spring的配置文件中配置其实现的关联。这样我们就可以在应用中调用Service接口来进行业务处理。Service层的业务实现,具体要调用到已定义的DAO层的接口,封装Service层的业务逻辑有利于通用的业务逻辑的独立性和重复利用性,程序显得非常简洁。
在项目中要降低耦合的话,分层是一种很好的概念,就是各层各司其职,尽量不做不相干的事,所以Service
层的话顾名思义就是业务逻辑,处理程序中的一些业务逻辑,以及调用DAO
层的代码,这里我们的DAo
层就是连接数据库的那一层,调用关系可以这样表达:
View(页面)>Controller(控制层)>Service(业务逻辑)>Dao(数据访问)>Database(数据库)
2. 编码
2.1 接口编码
首先还是接口的设计,设计Service秒杀商品的接口 SeckillService
首先在som.suny
包下建立interfaces
这个包,这个包里面存放Service
相关的接口,然后建立SeckillService
接口文件,代码如下:
public interface SeckillService {
/**
* 查询全部的秒杀记录.
* @return 数据库中所有的秒杀记录
*/
List<Seckill> getSeckillList();
/**
* 查询单个秒杀记录
* @param seckillId 秒杀记录的ID
* @return 根据ID查询出来的记录信息
*/
Seckill getById(long seckillId);
/**
* 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址
* @param seckillId 秒杀商品Id
* @return 根据对应的状态返回对应的状态实体
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 执行秒杀操作,有可能是失败的,失败我们就抛出异常
* @param seckillId 秒杀的商品ID
* @param userPhone 手机号码
* @param md5 md5加密值
* @return 根据不同的结果返回不同的实体信息
*/
SeckillExecution executeSeckill(long seckillId,long userPhone,String md5)throws SeckillException,RepeatKillException,SeckillCloseException;
2.2 接口实现
建立后接口之后我们要写实现类了,在写实现类的时候我们肯定会碰到一个这样的问题,你要向前端返回json
数据的话,你是返回什么样的数据好?
直接返回一个数字状态码或者时文字?这样设计肯定是不好的,所以我们应该向前段返回一个实体信息json
,里面包含了一系列的信息。
无论是哪种状态都应该可以应对,既然是与数据库字段无关的类,那就不是PO
了,所以我们建立一个DTO
数据传输类,关于常见的几种对象我的解释如下:
- PO: 也就是我们在为每一张数据库表写一个实体的类
- VO, 对某个页面或者展现层所需要的数据,封装成一个实体类
- BO, 就是业务对象,我也不是很了解
- DTO, 跟VO的概念有点混淆,也是相当于页面需要的数据封装成一个实体类
- POJO, 简单的无规则java对象
在com.suny
下建立dto
包,然后建立Exposer
类,这个类是秒杀时数据库那边处理的结果的对象
public class Exposer {
/*是否开启秒杀 */
private boolean exposed;
/* 对秒杀地址进行加密措施 */
private String md5;
/* id为seckillId的商品秒杀地址 */
private long seckillId;
/* 系统当前的时间 */
private LocalDateTime now;
/* 秒杀开启的时间 */
private LocalDateTime start;
/* 秒杀结束的时间 */
private LocalDateTime end;
public Exposer() {
}
public Exposer(boolean exposed, String md5, long seckillId) {
this.exposed = exposed;
this.md5 = md5;
this.seckillId = seckillId;
}
public Exposer(boolean exposed, long seckillId, LocalDateTime now, LocalDateTime start, LocalDateTime 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 getSeckillId() {
return seckillId;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public LocalDateTime getNow() {
return now;
}
public void setNow(LocalDateTime now) {
this.now = now;
}
public LocalDateTime getStart() {
return start;
}
public void setStart(LocalDateTime start) {
this.start = start;
}
public LocalDateTime getEnd() {
return end;
}
public void setEnd(LocalDateTime end) {
this.end = end;
}
@Override
public String toString() {
return "Exposer{" +
"秒杀状态=" + exposed +
", md5加密值='" + md5 + '\'' +
", 秒杀ID=" + seckillId +
", 当前时间=" + now +
", 开始时间=" + start +
", 结束=" + end +
'}';
}
}
然后我们给页面返回的数据应该是更加友好的封装数据,所以我们再在com.suny.dto
包下再建立SeckillExecution
用来封装给页面的结果:
public class SeckillExecution {
private long seckillId;
/* 执行秒杀结果的状态 */
private int state;
/* 状态的明文标示 */
private String stateInfo;
/* 当秒杀成功时,需要传递秒杀结果的对象回去 */
private SuccessKilled successKilled;
/* 秒杀成功返回的实体 */
public SeckillExecution(long seckillId, int state, String stateInfo, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = state;
this.stateInfo = stateInfo;
this.successKilled = successKilled;
}
/* 秒杀失败返回的实体 */
public SeckillExecution(long seckillId, int state, String stateInfo) {
this.seckillId = seckillId;
this.state = state;
this.stateInfo = stateInfo;
}
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 getSuccessKilled() {
return successKilled;
}
public void setSuccessKilled(SuccessKilled successKilled) {
this.successKilled = successKilled;
}
@Override
public String toString() {
return "SeckillExecution{" +
"秒杀的商品ID=" + seckillId +
", 秒杀状态=" + state +
", 秒杀状态信息='" + stateInfo + '\'' +
", 秒杀的商品=" + successKilled +
'}';
}
}
3.定义秒杀中可能会出现的异常
定义一个基础的异常,所有的子异常继承这个异常SeckillException
/**
* 秒杀基础异常
* Created by on 17-5-23.下午8:24
*/
public class SeckillException extends RuntimeException {
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
首选可能会出现秒杀关闭后被秒杀情况,所以建立秒杀关闭异常SeckillCloseException
,需要继承我们一开始写的基础异常
/**
* 秒杀已经关闭异常,当秒杀结束就会出现这个异常
* Created by on 17-5-23.下午8:27
*/
public class SeckillCloseException extends SeckillException{
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
然后还有可能发生重复秒杀异常RepeatKillException
/**
* 重复秒杀异常,不需要我们手动去try catch
* Created by on 17-5-23.下午8:26
*/
public class RepeatKillException extends SeckillException{
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
4.实现Service
接口
/**
* Created by on 17-5-23.下午9:30
*/
@Service
public class SeckillServiceImpl implements SeckillService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/* 加入一个盐值,用于混淆*/
private final String salt = "thisIsASaltValue";
@Autowired
private SeckillMapper seckillMapper;
@Autowired
private SuccessKilledMapper successKilledMapper;
/**
* 查询全部的秒杀记录.
*
* @return 数据库中所有的秒杀记录
*/
@Override
public List<Seckill> getSeckillList() {
return seckillMapper.queryAll(0, 4);
}
/**
* 查询单个秒杀记录
*
* @param seckillId 秒杀记录的ID
* @return 根据ID查询出来的记录信息
*/
@Override
public Seckill getById(long seckillId) {
return seckillMapper.queryById(seckillId);
}
/**
* 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址
*
* @param seckillId 秒杀商品Id
* @return 根据对应的状态返回对应的状态实体
*/
@Override
public Exposer exportSeckillUrl(long seckillId) {
// 根据秒杀的ID去查询是否存在这个商品
/* Seckill seckill = seckillMapper.queryById(seckillId);
if (seckill == null) {
logger.warn("查询不到这个秒杀产品的记录");
return new Exposer(false, seckillId);
}*/
Seckill seckill = redisDao.getSeckill(seckillId);
if (seckill == null) {
// 访问数据库读取数据
seckill = seckillMapper.queryById(seckillId);
if (seckill == null) {
return new Exposer(false, seckillId);
} else {
// 放入redis
redisDao.putSeckill(seckill);
}
}
// 判断是否还没到秒杀时间或者是过了秒杀时间
LocalDateTime startTime = seckill.getStartTime();
LocalDateTime endTime = seckill.getEndTime();
LocalDateTime nowTime = LocalDateTime.now();
// 开始时间大于现在的时候说明没有开始秒杀活动 秒杀活动结束时间小于现在的时间说明秒杀已经结束了
/* if (!nowTime.isAfter(startTime)) {
logger.info("现在的时间不在开始时间后面,未开启秒杀");
return new Exposer(false, seckillId, nowTime, startTime, endTime);
}
if (!nowTime.isBefore(endTime)) {
logger.info("现在的时间不在结束的时间之前,可以进行秒杀");
return new Exposer(false, seckillId, nowTime, startTime, endTime);
}*/
if (nowTime.isAfter(startTime) && nowTime.isBefore(endTime)) {
//秒杀开启,返回秒杀商品的id,用给接口加密的md5
String md5 = getMd5(seckillId);
return new Exposer(true, md5, seckillId);
}
return new Exposer(false, seckillId, nowTime, startTime, endTime);
}
private String getMd5(long seckillId) {
String base = seckillId + "/" + salt;
return DigestUtils.md5DigestAsHex(base.getBytes());
}
/**
* 执行秒杀操作,失败的,失败我们就抛出异常
*
* @param seckillId 秒杀的商品ID
* @param userPhone 手机号码
* @param md5 md5加密值
* @return 根据不同的结果返回不同的实体信息
*/
@Override
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException {
if (md5 == null || !md5.equals(getMd5(seckillId))) {
logger.error("秒杀数据被篡改");
throw new SeckillException("seckill data rewrite");
}
// 执行秒杀业务逻辑
LocalDateTime nowTIme = LocalDateTime.now();
try {
//执行减库存操作
int reduceNumber = seckillMapper.reduceNumber(seckillId, nowTIme);
if (reduceNumber <= 0) {
logger.warn("没有更新数据库记录,说明秒杀结束");
throw new SeckillCloseException("seckill is closed");
} else {
// 这里至少减少的数量不为0了,秒杀成功了就增加一个秒杀成功详细
int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone);
// 查看是否被重复插入,即用户是否重复秒杀
if (insertCount <= 0) {
throw new RepeatKillException("seckill repeated");
} else {
// 秒杀成功了,返回那条插入成功秒杀的信息
SuccessKilled successKilled = successKilledMapper.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId,1,"秒杀成功");
}
}
} catch (SeckillCloseException | RepeatKillException e1) {
throw e1;
} catch (Exception e) {
logger.error(e.getMessage(), e);
// 把编译期异常转换为运行时异常
throw new SeckillException("seckill inner error : " + e.getMessage());
}
}
在这里我们捕获了运行时异常,这样做的原因就是Spring
的事物默认就是发生了RuntimeException
才会回滚,可以检测出来的异常是不会导致事物的回滚的,这样的目的就是你明知道这里会发生异常,所以你一定要进行处理。
如果只是为了让编译通过的话,那捕获异常也没多意思,所以这里要注意事物的回滚.
然后我们还发现这里存在硬编码的现象,就是返回各种字符常量,例如秒杀成功
,秒杀失败
等等,这些字符串时可以被重复使用的,而且这样维护起来也不方便,要到处去类里面寻找这样的字符串,所有我们使用枚举类来管理这样状态,在con.suny
包下建立enum
包,专门放置枚举类,然后再建立SeckillStatEnum
枚举类:
/**
* 常量枚举类
* Created by on 17-5-23.下午10:15
*/
public enum SeckillStatEnum {
SUCCESS(1, "秒杀成功"),
END(0, "秒杀结束"),
REPEAT_KILL(-1, "重复秒杀"),
INNER_ERROR(-2, "系统异常"),
DATE_REWRITE(-3, "数据篡改");
private int state;
private String info;
SeckillStatEnum() {
}
SeckillStatEnum(int state, String info) {
this.state = state;
this.info = info;
}
public int getState() {
return state;
}
public String getInfo() {
return info;
}
public static SeckillStatEnum stateOf(int index) {
for (SeckillStatEnum statEnum : values()) {
if (statEnum.getState() == index) {
return statEnum;
}
}
return null;
}
}
既然把这些改成了枚举,那么在SeckillServiceImpl
类中的executeSeckill
方法中成功秒杀的返回值就应该修改为
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
改了这里以后会发现会报错,因为在实体类那边构造函数可不是这样的,然后修改SeckillExecution
类的构造函数,把state
跟stateInfo
的值设置从构造函数里面的SeckillStatEnum
中取出值来设置:
/* 秒杀成功返回的实体 */
public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getInfo();
this.successKilled = successKilled;
}
/* 秒杀失败返回的实体 */
public SeckillExecution(long seckillId, SeckillStatEnum statEnum) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getInfo();
}
5.注入Service
首先在resources/spring
下建立applicationContext-service.xml
文件,用来配置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.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包下的注解,在这里配置了自动扫描后,com.suny.service包下所有带有@Service注解的类都会被加入Spring容器中-->
<context:component-scan base-package="com.suny.service"/>
<!--配置事物,这里时使用基于注解的事物-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--开启基于注解的申明式事物-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
在这里开启了基于注解的事物,常见的事物操作有以下几种方法:
- 在Spring早期版本中是使用ProxyFactoryBean+XMl方式来配置事物.
- 在Spring配置文件使用tx:advice+aop命名空间,好处就是一次配置永久生效,你无须去关心中间出的问题,不过出错了你很难找出来在哪里出了问题
- 注解@Transactional的方式,注解可以在
方法定义
,接口定义
,类定义
,public方法上
,但是不能注解在private
,final
,static
等方法上,因为Spring的事物管理默认是使用Cglib动态代理的:- private方法因为访问权限限制,无法被子类覆盖
- final方法无法被子类覆盖
- static时类级别的方法,无法被子类覆盖
- protected方法可以被子类覆盖,因此可以被动态字节码增强
不能被Spring AOP事物增强的方法:
序号 | 动态代理策略 | 不能被事物增强的方法 |
---|---|---|
1 | 基于接口的动态代理 | 出了public以外的所有方法,并且 public static 的方法也不能被增强 |
2 | 基于Cglib的动态代理 | private,static,final的方法 |
然后你要在Service
类上添加注解@Service
,不用在接口上添加注解:
@Service
public class SeckillServiceImpl implements SeckillService
既然已经开启了基于注解的事物,那我们就去需要被事物的方法上加个注解@Transactional
:
@Transactional
@Override
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException
6.Service层的测试
写测试类,我这里的测试类名为SeckillServiceImplTest
:
/**
* Created by on 17-5-23.下午10:30
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/applicationContext-dao.xml", "classpath:spring/applicationContext-service.xml"})
public class SeckillServiceImplTest {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private SeckillService seckillService;
@Test
public void getSeckillList() throws Exception {
List<Seckill> seckillList = seckillService.getSeckillList();
logger.info(seckillList.toString());
System.out.println(seckillList.toString());
}
@Test
public void getById() throws Exception {
long seckillId = 1000;
Seckill byId = seckillService.getById(seckillId);
System.out.println(byId.toString());
}
@Test
public void exportSeckillUrl() throws Exception {
long seckillId = 1000;
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
System.out.println(exposer.toString());
}
@Test
public void executeSeckill() throws Exception {
long seckillId = 1000;
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
if (exposer.isExposed()) {
long userPhone = 12222222222L;
String md5 = "bf204e2683e7452aa7db1a50b5713bae";
try {
SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5);
System.out.println(seckillExecution.toString());
} catch (SeckillCloseException | RepeatKillException e) {
e.printStackTrace();
}
} else {
System.out.println("秒杀未开启");
}
}
@Test
public void executeSeckillProcedureTest() {
long seckillId = 1001;
long phone = 1368011101;
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
if (exposer.isExposed()) {
String md5 = exposer.getMd5();
SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
System.out.println(execution.getStateInfo());
}
}
}
测试的话如果每个方法测试都通过就说明通过,如果报错了话就仔细看下哪一步错了检查下