Service层的设计
创建需要的包
-
service包:存放service的接口和实现类
-
exception包:存放service所存在的一些异常(重复秒杀,秒杀关闭等)
-
dto包:也是存放数据,和entity的区别在于,entity是业务的封装,dto是web和service之间的数据传递
接口SeckillService
站在使用者的角度去设计接口,而不是实现;使用者使用越方便越好
1. 方法定义粒度 2. 参数 3. 返回类型
- 查询所有的秒杀记录(为的是要有一个列表页,展示所有秒杀)
List<Seckill> getSeckillList();
- 查询单个秒杀记录
/**
* 查询单个秒杀记录
* @param seckillId
* @return
*/
Seckill getById(long seckillId);
- 输出秒杀接口的地址
① 秒杀开始的时候输出秒杀的接口地址
② 否则输出的是系统的时间和秒杀的时间
这样的话秒杀没有开始的时候,谁也不知道我们的秒杀地址,用户没法提前猜到我们秒杀的地址,而提前使用浏览器插件等待秒杀
//秒杀开始输出秒杀接口地址
//否则输出系统的时间和秒杀时间
Exposer exportSeckillUrl(long seckillId);
这里的返回值Exposer是我们自己封装的dto对象,和业务无关,只是为了数据的传输更方便
package org.seckill.dto;
public class Exposer {
//是否开启秒杀
private boolean exposed;
//一种加密地址
private String md5;
//如果秒杀开始了就返回秒杀地址,也就是被描述的id
private long seckillId;
//秒杀没开始就不返回地址,返回秒杀的开始时间和结束时间
//系统当前时间
private long now;
//秒杀开启时间
private long start;
//秒杀结束时间
private long end;
public Exposer(boolean exposed, String md5, long seckillId) {
this.exposed = exposed;
this.md5 = md5;
this.seckillId = seckillId;
}
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 getSeckillId() {
return seckillId;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
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;
}
}
- 执行秒杀
这一接口应该是在获取了秒杀的接口地址后进行,使用md5加密和内部的规则作比较,防止用户的url被篡改
/**
* 执行秒杀
* @param seckillId
* @param userPhone
* @param md5:md5加密和内部的规则作比较,防止用户的url被篡改
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException ;
返回值SeckillExecution也是我们自己封装的dto对象,如果秒杀成功返回秒杀Id,成功标识和成功对象,失败则不返回秒杀对象
package org.seckill.dto;
import org.seckill.entity.SuccessKilled;
/**
* 封装秒杀结果
*/
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;
}
}
在执行秒杀的时候会有两个异常,重复秒杀异常和秒杀关闭异常
这都是运行时异常,即便不进行try catch,也不会有编译错误,而spring管理事务也都只能帮助我们回滚运行是异常
在我们设计的时候,应该有一个所有的秒杀相关业务的异常作为父类,其他异常由他细分
① 所有的秒杀相关业务的异常SeckillException
package org.seckill.exception;
/**
* 所有的秒杀相关业务的异常
*/
public class SeckillException extends RuntimeException{
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
② 重复秒杀异常RepeatKillException
package org.seckill.exception;
/**
* 重复秒杀异常(运行期异常)
*/
public class RepeatKillException extends SeckillException{
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
③ 秒杀关闭异常SeckillCloseException
package org.seckill.exception;
public class SeckillCloseException extends SeckillException {
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
完整代码
package org.seckill.service;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
import java.util.List;
public interface SeckillService {
List<Seckill> getSeckillList();
/**
* 查询单个秒杀记录
* @param seckillId
* @return
*/
Seckill getById(long seckillId);
//秒杀开始输出秒杀接口地址
//否则输出系统的时间和秒杀时间
Exposer exportSeckillUrl(long seckillId);
/**
* 执行秒杀
* @param seckillId
* @param userPhone
* @param md5:md5加密和内部的规则作比较,防止用户的url被篡改
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException ;
}
SeckillService的实现SeckillServiceImpl
实现放在service下的impl包下
- 返回秒杀对象的方法很简单,之间调用dao层的方法即可
@Override
public List<Seckill> getSeckillList() {
//只有4个秒杀对象
return seckillDao.queryAll(0,4);
}
@Override
public Seckill getById(long seckillId) {
return seckillDao.queryById(seckillId);
}
- exportSeckillUrl接口的实现
@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);
}
其中把生成MD5单独抽出一个方法,为了在executeSeckill方法的重用
声明一个md5盐值用于混淆md5,因为如果只对skillId加密,很容易被猜出来,使用盐值和skillId的特定拼接再生成MD5更安全
private final String slat="sdaqei012ee[qsdaq231w";
private String getMD5(long seckillId){
//md5盐值用于混淆md5,因为如果只对skillId加密,很容易被才出来
//使用盐值和skillId的特定拼接更安全
String base = seckillId+"/"+slat;
//使用spring的工具类生成md5
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
- executeSeckill接口的实现
这个就是对秒杀逻辑的执行
① 首先要去判断md5的正确性
if(md5 == null || !md5.equals(getMD5(seckillId))){
throw new SeckillException("seckill data rewrite");
}
② 接下来就是执行秒杀逻辑:减库存+记录购买记录
先去更新秒杀,没有更新到记录(商品没有了或者不在秒杀时间)则秒杀结束,抛出异常
int updateCount = seckillDao.reduceNumber(seckillId,nowTime);
if(updateCount <= 0){//没有更新到记录则秒杀结束
throw new SeckillCloseException("seckill is closed");
}
③ 更新秒杀成功后要去记录购买记录
int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
//userPhone和seckillId作为联合主键避免重复秒杀
if(insertCount<=0){
throw new RepeatKillException("seckill repeated");
}else {//秒杀成功
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId,1,"秒杀成功",successKilled);
}
④ 对于减库存和记录购买记录应该组成一个事务,要么都成功,要么都失败
@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 = successKilledDao.insertSuccessKilled(seckillId,userPhone);
//userPhone和seckillId作为联合主键避免重复秒杀
if(insertCount<=0){
throw new RepeatKillException("seckill repeated");
}else {//秒杀成功
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId,1,"秒杀成功",successKilled);
}
}
}catch (SeckillCloseException e1){
throw e1;
} catch (RepeatKillException e2){
throw e2;
}catch (Exception e){
logger.error(e.getMessage(),e);
throw new SeckillException("seckill inner error:"+e.getMessage());
}
}
⑤ 逻辑实现完成,但是并不够友好,也就是说,new SeckillExecution(seckillId,1,“秒杀成功”,successKilled);里面的1和“秒杀成功”应该都是常量,我们用枚举来记录,更易于组织
枚举类如下
package org.seckill.enums;
public enum SeckillStaEnum {
SUCCESS(1,"秒杀成功"),
END(0,"秒杀结束"),
REPEAT_KILL(-1,"重复秒杀"),
INNER_ERROR(-2,"系统异常"),
DATA_REWRITE(-3,"数据篡改");
private int state;
private String stateInf;
SeckillStaEnum(int state, String stateInf) {
this.state = state;
this.stateInf = stateInf;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getStateInf() {
return stateInf;
}
public void setStateInf(String stateInf) {
this.stateInf = stateInf;
}
public static SeckillStaEnum stateOf(int index){
for (SeckillStaEnum state : values()) {
if(state.getState() == index){
return state;
}
}
return null;
}
}
更改SeckillExecution的构造方法
public SeckillExecution(long seckillId, SeckillStaEnum seckillStaEnum, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = seckillStaEnum.getState();
this.stateInfo = seckillStaEnum.getStateInf();
this.successKilled = successKilled;
}
public SeckillExecution(long seckillId, SeckillStaEnum seckillStaEnum) {
this.seckillId = seckillId;
this.state = seckillStaEnum.getState();
this.stateInfo = seckillStaEnum.getStateInf();
}
更改后完整代码如下
@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 = successKilledDao.insertSuccessKilled(seckillId,userPhone);
//userPhone和seckillId作为联合主键避免重复秒杀
if(insertCount<=0){
throw new RepeatKillException("seckill repeated");
}else {//秒杀成功
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStaEnum.SUCCESS,successKilled);
}
}
}catch (SeckillCloseException e1){
throw e1;
} catch (RepeatKillException e2){
throw e2;
}catch (Exception e){
logger.error(e.getMessage(),e);
throw new SeckillException("seckill inner error:"+e.getMessage());
}
}
使用Spring托管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"
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">
<!--扫描service包下所有使用注解的类型-->
<context:component-scan base-package="org.seckill.service"></context:component-scan>
</beans>
Spring声明式事务
我们不用再去关心什么时候开始事务,什么时候结束事务,什么时候回滚,而都交给spring管理
事务方法的嵌套
当新的事务加进来的时候,如果之前的有事务则直接加入原有的事务逻辑,没有则创建新的事务
什么时候回滚
当抛出运行期异常的时候就会回滚
配置
<?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"></context:component-scan>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource">
</property>
</bean>
<!--配置基于注解的声明事务-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
最终的service实现类
package org.seckill.service.impl;
import org.seckill.dao.SeckillDao;
import org.seckill.dao.SuccessKilledDao;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.entity.SuccessKilled;
import org.seckill.enums.SeckillStaEnum;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import javax.xml.crypto.Data;
import java.util.Date;
import java.util.List;
@Service
public class SeckillServiceImpl implements SeckillService {
//日志对象
private Logger logger = LoggerFactory.getLogger(this.getClass());
//实现类在容器当中,注入dao层
@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;
//md5盐值用于混淆md5,因为如果只对skillId加密,很容易被才出来
//使用盐值和skillId的特定拼接更安全
private final String slat="sdaqei012ee[qsdaq231w";
@Override
public List<Seckill> getSeckillList() {
//只有4个秒杀对象
return seckillDao.queryAll(0,4);
}
@Override
public Seckill getById(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);
}
@Override
@Transactional
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 = successKilledDao.insertSuccessKilled(seckillId,userPhone);
//userPhone和seckillId作为联合主键避免重复秒杀
if(insertCount<=0){
throw new RepeatKillException("seckill repeated");
}else {//秒杀成功
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStaEnum.SUCCESS,successKilled);
}
}
}catch (SeckillCloseException e1){
throw e1;
} catch (RepeatKillException e2){
throw e2;
}catch (Exception e){
logger.error(e.getMessage(),e);
throw new SeckillException("seckill inner error:"+e.getMessage());
}
}
private String getMD5(long seckillId){
//md5盐值用于混淆md5,因为如果只对skillId加密,很容易被才出来
//使用盐值和skillId的特定拼接更安全
String base = seckillId+"/"+slat;
//使用spring的工具类生成md5
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
}