Java高并发秒杀——Service层
Service层:完成DAO层的拼接以及其他逻辑
Service层分析目录
1、Service层接口设计与实现
2、Spring装配Service实现类
3、Spring声明式事务配置
4、Junit单元测试Service层
一、Service层接口设计与实现
1、创建业务接口:站在使用者的角度设计接口,三个方面:方法定义粒度,参数,返回类型(return 类型/异常)
package com.fehead.service;
import com.fehead.bean.Seckill;
import com.fehead.dto.Exposer;
import com.fehead.dto.SeckillExecution;
import com.fehead.exception.RepeatKillException;
import com.fehead.exception.SeckillCloseException;
import com.fehead.exception.SeckillException;
import javax.swing.text.html.parser.Entity;
import java.util.List;
/**
* Created by xiaoaxiao on 2019/5/3
* Description:业务接口:站在“使用者”角度设计接口
* 三个方面:方法定义粒度,参数,返回类型(return 类型/异常)
*/
public interface SeckillService {
/**
* 查询所有秒杀记录
* @return
*/
List<Seckill> getSeckillList();
/**
* 查询单个秒杀记录
* @param seckillId
* @return
*/
Seckill getSeckillById(long seckillId);
/**
* 秒杀开启时输出秒杀接口地址,秒杀存在但未开启时输出系统时间和秒杀时间,秒杀不存在直接返回id就行
* ——这可以通过创建多个Exposer的构造器来实现
* 当web层或接口使用方调用该方法时,可以拿到Exposer的dto,可以看到一些返回的数据
* @param seckillId
* @return
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 执行秒杀操作
* @param seckillId
* @param userPhone
* @param md5
* @return
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, SeckillCloseException, RepeatKillException;
//抛出SeckillCloseException, RepeatKillException异常需要明确告诉用户或接口使用者,秒杀关闭异常/重复秒杀异常
//而抛出SeckillException异常只需要告诉用户秒杀存在异常,秒杀失败
/**
* 执行秒杀操作by 存储过程
* @param seckillId
* @param userPhone
* @param md5
* @return
*/
SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5)
throws SeckillException, SeckillCloseException, RepeatKillException;
}
通过创建多个Exposer构造器实现秒杀的不同情况:1、秒杀开启时输出秒杀接口地址;2、秒杀存在但未开启时输出系统时间和秒杀时间;3、秒杀不存在时直接返回id
package com.fehead.dto;
/**
* Created by xiaoaxiao on 2019/5/3
* Description:暴露秒杀地址DTO
* DTO层:放与大部分业务不相关,方便Service层返回的数据的封装
*/
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;
}
@Override
public String toString() {
return "Exposer{" +
"exposed=" + exposed +
", md5='" + md5 + '\'' +
", seckillId=" + seckillId +
", now=" + now +
", start=" + start +
", end=" + end +
'}';
}
}
package com.fehead.dto;
import com.fehead.bean.SuccessKilled;
import com.fehead.enums.SeckillStatEnum;
/**
* Created by xiaoaxiao on 2019/5/3
* Description:封装秒杀完成后的结果
*/
public class SeckillExecution {
private long seckillId;
private int state;
private String stateInfo;
private SuccessKilled successKilled;
public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getStateInfo();
this.successKilled = successKilled;
}
public SeckillExecution(long seckillId, SeckillStatEnum statEnum) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.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 getSuccessKilled() {
return successKilled;
}
public void setSuccessKilled(SuccessKilled successKilled) {
this.successKilled = successKilled;
}
@Override
public String toString() {
return "SeckillExecution{" +
"seckillId=" + seckillId +
", state=" + state +
", stateInfo='" + stateInfo + '\'' +
", successKilled=" + successKilled +
'}';
}
}
package com.fehead.exception;
/**
* Created by xiaoaxiao on 2019/5/3
* Description:秒杀相关业务异常,所以其他异常(重复异常/关闭异常)都是相关业务异常的子类
*/
public class SeckillException extends RuntimeException{
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
package com.fehead.exception;
/**
* Created by xiaoaxiao on 2019/5/3
* Description:重复秒杀异常(运行期异常)
*/
public class RepeatKillException extends SeckillException {
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
package com.fehead.exception;
/**
* Created by xiaoaxiao on 2019/5/3
* Description:
*/
public class SeckillCloseException extends SeckillException {
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
package com.fehead.enums;
/**
* Created by xiaoaxiao on 2019/5/3
* Description:使用枚举表述常量数据字段
*/
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;
}
}
package com.fehead.service.Impl;
import com.fehead.bean.Seckill;
import com.fehead.bean.SuccessKilled;
import com.fehead.dao.SeckillDao;
import com.fehead.dao.SuccessKilledDao;
import com.fehead.dao.cache.RedisDao;
import com.fehead.dto.Exposer;
import com.fehead.dto.SeckillExecution;
import com.fehead.enums.SeckillStatEnum;
import com.fehead.exception.RepeatKillException;
import com.fehead.exception.SeckillCloseException;
import com.fehead.exception.SeckillException;
import com.fehead.service.SeckillService;
import org.apache.commons.collections.MapUtils;
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 java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by xiaoaxiao on 2019/5/3
* Description:
*/
@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 = "uihwugwmo%$^&*(*#*(&";
public List<Seckill> getSeckillList() {
return seckillDao.queryAll(0, 4);
}
public Seckill getSeckillById(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);
}
/**
* 通过seckillId获取一个独特的md5,md5的生成过程为 盐值字符串+base的规则,双重保障来完成md5的加密
*
* @param seckillId
* @return
*/
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, SeckillCloseException, RepeatKillException {
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);
//唯一:seckillId,userPhone
if (insertCount <= 0) {
//重复秒杀,sql语句中的ignore就会返回0
throw new RepeatKillException("seckill repeat");
} else {
//秒杀成功
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());
}
}
//通过redis+数据库中的存储过程来对秒杀过程进行优化
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) throws SeckillException, SeckillCloseException, RepeatKillException {
if (md5 == null || !md5.equals(getMD5(seckillId))) {
return new SeckillExecution(seckillId, SeckillStatEnum.DATA_REWRITE);
}
Date killTime = new Date();
Map<String, Object> map = new HashMap<String, Object>();
map.put("seckillId", seckillId);
map.put("phone", userPhone);
map.put("killTime", killTime);
map.put("result", null);
//执行存储过程,result被复制
try {
seckillDao.killByProcedure(map);
//获取result
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);
}
}
}
二、Spring装配Service实现类
<!-- 扫描service包及其子包下所有使用注解的类型-->
<context:component-scan base-package="com.fehead.service"/>
三、Spring声明式事务配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
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-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 扫描service包及其子包下所有使用注解的类型-->
<context:component-scan base-package="com.fehead.service"/>
<!-- 配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置基于注解的声明式事务
默认使用注解来管理事务行为
-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
四、Junit单元测试Service层
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<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>
package com.fehead.service;
import com.fehead.bean.Seckill;
import com.fehead.dto.Exposer;
import com.fehead.dto.SeckillExecution;
import com.fehead.exception.RepeatKillException;
import com.fehead.exception.SeckillCloseException;
import org.junit.Test;
import org.junit.runner.RunWith;
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;
import static org.junit.Assert.*;
/**
* Created by xiaoaxiao on 2019/5/3
* Description:
*/
@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() {
List<Seckill> list = seckillService.getSeckillList();
logger.info("list={}", list);
}
@Test
public void getSeckillById() {
long id = 1000;
Seckill seckill = seckillService.getSeckillById(id);
logger.info("seckill={}",seckill);
}
@Test
/**
* Exposer{
* exposed=true,
* md5='fe95f6f9363d3286ddcb4f93e6082a4c',
* seckillId=1000,
* now=0, start=0, end=0}
*/
public void exportSeckillUrl() {
long id = 1000;
//修改数据库把活动开启时间改为2019.5.1-2019.11.29
Exposer exposer = seckillService.exportSeckillUrl(id);
logger.info("exposer={}",exposer);
}
@Test
/**
* SeckillExecution{seckillId=1000, state=1, stateInfo='秒杀成功',
* successKilled=SuccessKilled{seckillId=1000, userPhone=12345678911,
* state=0, createTime=Fri May 03 22:18:31 CST 2019, seckill=Seckill{
* seckillId=1000, name='1000元秒杀iphone6', number=99, startTime=Fri May 03 22:18:31 CST 2019,
* endTime=Sat Nov 02 00:00:00 CST 2019, createTime=Mon Apr 29 16:46:23 CST 2019}}}
*/
public void executeSeckill() {
long id = 1000;
long phone = 12345678911L;
String md5 = "fe95f6f9363d3286ddcb4f93e6082a4c";
//通过try-catch对已定义好了的异常进行相应的处理,就不会乱报错了
try {
SeckillExecution seckillExecution = seckillService.executeSeckill(id,phone,md5);
logger.info("result={}",seckillExecution);
}catch (RepeatKillException e){
logger.error(e.getMessage());
}catch (SeckillCloseException e){
logger.error(e.getMessage());
}
}
@Test
/**
* 集成测试代码完整逻辑,注意可重复执行——上面两个方法加起来,直接使用md5,而不是复制
*/
public void testSeckillLogic() throws Exception{
long id = 1001;
Exposer exposer = seckillService.exportSeckillUrl(id);
if(exposer.isExposed()){
logger.info("exposer={}",exposer);
long phone = 12345678911L;
String md5 = "fe95f6f9363d3286ddcb4f93e6082a4c";
//通过try-catch对已定义好了的异常进行相应的处理,就不会乱报错了
try {
SeckillExecution seckillExecution = seckillService.executeSeckill(id,phone,md5);
logger.info("result={}",seckillExecution);
}catch (RepeatKillException e){
logger.error(e.getMessage());
}catch (SeckillCloseException e){
logger.error(e.getMessage());
}
}else {
//秒杀未开启
logger.warn("exposer={}",exposer);
}
}
@Test
public void executeSeckillProcedureTest(){
long seckillId = 1001;
long phone = 13687945684L;
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
if(exposer.isExposed()){
String md5 = exposer.getMd5();
SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId,phone,md5);
logger.info(execution.getStateInfo());
}
}
}