项目源码:https://gitee.com/zsmsy_admin/Seckill
题外话:哈,这篇博客好久之前就应该要写完的了,之前写到一半就被晾在那了=。 =(主要还是自己拖延症又犯了)
在Dao层我们只完成了针对表的相关操作包括写了接口方法和映射文件中的sql语句,并没有编写逻辑的代码,例如对多个Dao层方法的拼接,当我们用户成功秒杀商品时我们需要进行商品的减库存操作(调用SeckillDao接口)和增加用户明细(调用SuccessKilledDao接口),这些逻辑我们都需要在Service层完成。这也是一些初学者容易出现的错误,他们喜欢在Dao层进行逻辑的编写,其实Dao就是数据访问的缩写,它只进行数据的访问操作,接下来我们便进行Service层代码的编写。
1.秒杀Service接口设计
在org.learn包下创建service用于存放service接口及实现类,创建一个exception包用于存放service层出现的异常例如重复秒杀商品异常、秒杀已关闭、秒杀未开始等异常,再创建一个dto包作为传输层,dto和entity的区别在于:entity用于业务数据的封装,而dto用于完成web和service层的数据传递。
首先我们需要在service包下创建Service接口,里面的方法应该是按”使用者”(程序员)的角度去设计,SeckillService.java,代码如下:
public interface SeckillService {
/**
* 查询所有秒杀记录
* @return
*/
List getSeckillList();
/**
* 查询单个秒杀记录
* @param seckillId
* @return
*/
Seckill getById(long seckillId);
/**
* 在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间
* @param seckillId
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 执行秒杀操作
* @param seckillId
* @param userPhone
* @param md5
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException,RepeatKill2shenyuException,SeckillCloseException;
/**
* 执行秒杀操作 by 存储过程
* @param seckillId
* @param userPhone
* @param md5
* @return
* @throws SeckillException
* @throws RepeatKillException
* @throws SeckillCloseException
*/
SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5)
throws SeckillException,RepeatKillException,SeckillCloseException;
}
这个接口前面两个方法返回的都是跟我们业务相关的对象,第3个方法是防止他人在秒杀开启时间前通过接口地址秒杀,第四个方法是业务中最重要的操作,该方法主要是执行秒杀操作返回秒杀结果。另外第五个方法实际上作用与第四个一样,是后来为优化高并发问题,通过“存储过程”的方法对第四个方法进行优化。
2.秒杀Service接口的实现
Service接口设计完成后,需要对接口进行实现,我们在service包下创建impl包用来存放实现类。SeckillServiceImpl.java,内容如下:
@Service
public class SeckillServiceImpl implements SeckillService{
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;
@Autowired
private RedisDao redisDao;
//MD5盐值字符串,用于混淆md5
private final String slat = "sdaholskay9729e172931009aS8D09_@_!#!@3";
public List getSeckillList() {
return seckillDao.queryAll(0,4);
}
public Seckill getById(long seckillId) {
return seckillDao.queryById(seckillId);
}
public Exposer exportSeckillUrl(long seckillId) {
//1,访问redis
Seckill seckill = redisDao.getSeckill(seckillId);
if (seckill == null){
//2,访问数据库
seckill = seckillDao.queryById(seckillId);
if (seckill == null){
return new Exposer(false, seckillId);
}else {
//3,放入redis
redisDao.putSeckill(seckill);
}
}
// 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;
}
//秒杀是否成功,成功:减库存,增加明细;失败:抛出异常,事务回滚
@Transactional
/**
* 使用注解控制事务方法的优点:
* 1.开发团队达成一致约定,明确标注事务方法的编程风格
* 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
* 3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制
*/
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 insertCount=successKilledDao.insertSuccessKilled(seckillId,userPhone);
//看是否该明细被重复插入,即用户是否重复秒杀
if (insertCount<=0)
{
throw new RepeatKillException("seckill repeated");
}else {
//减库存,热点商品竞争
int updateCount=seckillDao.reduceNumber(seckillId,nowTime);
if (updateCount<=0)
{
//没有更新库存记录,说明秒杀结束 rollback
throw new SeckillCloseException("seckill is closed");
}else {
//秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commit
SuccessKilled successKilled=successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
return new SeckillExecution(seckillId, SeckillStatEnum.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());
}
}
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5){
if(md5 == null || !md5.equals(getMD5(seckillId))){
return new SeckillExecution(seckillId,SeckillStatEnum.DATA_REWRITE);
}
Date killTime = new Date();
Map map = new HashMap();
map.put("seckillId",seckillId);
map.put("phone",userPhone);
map.put("killTime",killTime);
map.put("result",null);
try{
seckillDao.killByProcedure(map);
int result = MapUtils.getInteger(map,"result",-2);
if(result == 1){
SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
return new SeckillExecution(seckillId,SeckillStatEnum.SUCCESS,sk);
}else {
return new SeckillExecution(seckillId,SeckillStatEnum.stateOf(result));
}
} catch (Exception e){
logger.error(e.getMessage(), e);
return new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);
}
}
}
上述代码是对service接口实现的过程,需要注意的是代码中返回参数信息给前端时,
return new SeckillExecution(seckillId,SeckillStatEnum.SUCCESS,successKilled);
我们不想在我们的return代码中硬编码这两个参数,所以我们应该考虑用枚举的方式将这些常量封装起来,在org.learn包下新建一个枚举包enums,创建一个枚举类型SeckillStatEnum.java,内容如下:
public enum SeckillStatEnum {
SUCCESS(1,"秒杀成功"),
END(0,"秒杀结束"),
REPEAT_KILL(-1,"重复秒杀"),
INNER_ERROR(-2,"系统异常"),
DATA_REWRITE(-3,"数据篡改");
private int state;
private String stateInfo;
SeckillStatEnum(int state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
}
public int getState() {
return state;
}
public String getStateInfo() {
return stateInfo;
}
public static SeckillStatEnum stateOf(int index) {
for (SeckillStatEnum state : values()) {
if (state.getState() == index) {
return state;
}
}
return null;
}
}
这样保证了一些常用常量数据被封装在枚举类型里。
目前为止我们Service的实现全部完成,接下来要将Service交给Spring的容器托管,进行一些配置。
3.使用Spring托管Service依赖配置
在spring包下创建一个spring-service.xml文件,内容如下:
<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">
<context:component-scan base-package="org.learn.service"/>
beans>
然后采用注解的方式将Service的实现类加入到Spring IOC容器中:
//@Component @Service @Dao @Controller
@Service
public class SeckillServiceImpl implements SeckillService
下面我们来运用Spring的声明式事务对我们项目中的事务进行管理。
4.使用Spring声明式事务配置
声明式事务的使用方式:1.早期使用的方式:ProxyFactoryBean+XMl.2.tx:advice+aop命名空间,这种配置的好处就是一次配置永久生效。3.注解@Transactional的方式。在实际开发中,建议使用第三种对我们的事务进行控制,优点见下面代码中的注释。下面让我们来配置声明式事务,在spring-service.xml中添加对事务的配置:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
bean>
然后在Service实现类的方法中,在需要进行事务声明的方法上加上事务的注解:
//秒杀是否成功,成功:减库存,增加明细;失败:抛出异常,事务回滚
@Transactional
/**
* 使用注解控制事务方法的优点:
* 1.开发团队达成一致约定,明确标注事务方法的编程风格
* 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
* 3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制
*/
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException {}
接下来就是对我们做的业务实现类来做集成测试。