本文为Java高并发秒杀API之Service层课程笔记。
编辑器:IDEA
java版本:java8
前文:秒杀系统环境搭建与DAO层设计
秒杀业务接口与实现
DAO层:接口设计、SQL编写
Service:业务,DAO拼接等逻辑
代码和SQL分离,方便review。
service接口设计
目录如下:
首先是SecKillService接口的设计:
public interface SecKillService {
List<SecKill> getSecKillList();
SecKill getById(long seckillId);
Exposer exportSecKillUrl(long seckillId);
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException,SeckillCloseException;
}
exportSecKillUrl 函数用来暴露出接口的地址,用一个专门的dto类Exposer 来实现:
public class Exposer {
private boolean exposed;
private String md5;
private long seckillId;
private long now;
private long start;
private long end;
}
这里有一个md5,是为了构造秒杀地址,防止提前猜出秒杀地址,执行作弊手段。
MD5是一个安全的散列算法,输入两个不同的明文不会得到相同的输出值,根据输出值,不能得到原始的明文,即其过程不可逆;所以要解密MD5没有现成的算法,只能用穷举法。
executeSeckill 函数表示执行秒杀,应该返回执行的结果相关信息:
public class SeckillExecution {
private long seckillId;
private int state;
private String stateInfo;
private SuccessKilled successKilled;
}
执行过程中可能会抛出异常。这里面用到了几个异常:SeckillException , RepeatKillException , SeckillCloseException 。
SeckillException.java,这个是其他两个的父类,除了那两个精确的异常,都可以返回这个异常。
public class SeckillException extends RuntimeException {
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
RepeatKillException是重复秒杀异常,一个用户一件商品只能秒杀一次:
public class RepeatKillException extends SeckillException {
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
同理,SeckillCloseException是秒杀关闭异常,秒杀结束了还在抢,返回异常。
public class SeckillCloseException extends SeckillException {
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
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 https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="cn.orzlinux.service"/>
</beans>
SecKillServiceImpl实现类实现了SecKillService接口的方法:指定日志对象,DAO对象。
@Service
public class SecKillServiceImpl implements SecKillService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private SecKillDao secKillDao;
@Resource
private SuccessKilledDao successKilledDao;
}
查询比较简单,直接调用DAO方法:
@Override
public List<SecKill> getSecKillList() {
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);
}
getMD5是一个自定义函数:
private final String slat="lf,ad.ga.dfgm;adrktpqerml[fasedfa]";
private String getMD5(long seckillId) {
String base = seckillId+"/orzlinux.cn/"+slat;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes(StandardCharsets.UTF_8));
return md5;
}
如果直接对密码进行散列,那么黑客可以对通过获得这个密码散列值,然后通过查散列值字典(例如MD5密码破解网站),得到某用户的密码。加Salt可以一定程度上解决这一问题。所谓加Salt方法,就是加点”佐料”。其基本想法是这样的:当用户首次提供密码时(通常是注册时),由系统自动往这个密码里撒一些“佐料”,然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的“佐料”,然后散列,再比较散列值,已确定密码是否正确。这里的“佐料”被称作“Salt值”,这个值是由系统随机生成的,并且只有系统知道。
执行秒杀函数,这里面牵扯到编译异常和运行时异常。
异常
编译时异常:编译成字节码过程中可能出现的异常。
运行时异常:将字节码加载到内存、运行类时出现的异常。
异常体系结构:
* java.lang.Throwable
* |-----java.lang.Error:一般不编写针对性的代码进行处理。
* |-----java.lang.Exception:可以进行异常的处理
* |------编译时异常(checked)
* |-----IOException
* |-----FileNotFoundException
* |-----ClassNotFoundException
* |------运行时异常(unchecked,RuntimeException)
* |-----NullPointerException
* |-----ArrayIndexOutOfBoundsException
* |-----ClassCastException
* |-----NumberFormatException
* |-----InputMismatchException
* |-----ArithmeticException
使用try-catch-finally 处理编译时异常,是得程序在编译时就不再报错,但是运行时仍可能报错。相当于我们使用try-catch-finally 将一个编译时可能出现的异常,延迟到运行时出现。开发中,由于运行时异常比较常见,所以我们通常就不针对运行时异常编写try-catch-finally 了。针对于编译时异常,我们说一定要考虑异常的处理。
executeSeckill.java
@Override
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws
SeckillException, RepeatKillException, SeckillException {
if(md5==null || !md5.equals(getMD5(seckillId))) {
throw new SeckillException("seckill data rewrite");
}
Date nowTime = new Date();
int updateCount = secKillDao.reduceNumber(seckillId,nowTime);
try {
if(updateCount<=0) {
throw new SeckillCloseException("seckill is closed");
} else {
int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
if(insertCount<=0) {
throw new RepeatKillException("seckill repeated");
} else {
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
return new SeckillExecution(seckillId, SecKillStatEnum.SUCCESS,successKilled);
}
}
} catch (SeckillCloseException | RepeatKillException e1){
throw e1;
} catch (Exception e) {
logger.error(e.getMessage(),e);
throw new SeckillException("seckill inner error"+e.getMessage());
}
}
SeckillCloseException 和RepeatKillException 都继承了运行时异常,所以这些操作把异常都转化为了运行时异常。这样spring才能回滚。数据库的修改才不会紊乱。
这里有一个操作就是枚举的使用。
return new SeckillExecution(seckillId, SecKillStatEnum.SUCCESS,successKilled);
用第一行的方式割裂了状态和状态信息,很不优雅,而且后续要更改的话,这些代码分散在各个代码中,不易修改,所以用枚举代替。
package cn.orzlinux.enums;
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 statEnum:values()) {
if(statEnum.getState()==index) {
return statEnum;
}
}
return null;
}
}
声明式事务
spring早期使用方式(2.0):ProxyFactoryBean + XML
后来:tx:advice+aop命名空间,一次配置永久生效。
注解@Transactional,注解控制。(推荐)
支持事务方法嵌套。
**何时回滚事务?**抛出运行期异常,小心try/catch
具体配置:
在spring-service.xml添加:
<bean id="transationManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transationManager"/>
在SecKillServiceImpl.java文件添加注解
@Override
@Transactional
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillException
...
使用注解控制事务方法的优点:
- 开发团队达成一致约定,明确标注事务方法的编程风格
- 保证事务方法的执行时间尽可能短,不要穿插其它网络操作,要剥离到事务外部
- 不是所有的方法都需要事务
集成测试
在resource文件夹下新建logback.xml,日志的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
</layout>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
SecKillServiceTest.java
@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 getById() {
long id = 1000;
SecKill secKill = secKillService.getById(id);
logger.info("seckill={}",secKill);
}
@Test
public void exportSecKillUrl() {
long id = 1000;
Exposer exposer = secKillService.exportSecKillUrl(id);
logger.info("exposer={}",exposer);
}
@Test
public void executeSeckill() {
long id = 1000;
long phone = 10134256781L;
String md5 = "c78a6784f8e8012796c934dbb3f76c03";
try {
SeckillExecution seckillExecution = secKillService.executeSeckill(id,phone,md5);
logger.info("result: {}",seckillExecution);
} catch (RepeatKillException | SeckillCloseException e) {
logger.error(e.getMessage());
}
}
@Test
public void testSeckillLogic() {
long id = 1001;
Exposer exposer = secKillService.exportSecKillUrl(id);
if(exposer.isExposed()) {
logger.info("exposer={}",exposer);
long phone = 10134256781L;
String md5 = exposer.getMd5();
try {
SeckillExecution seckillExecution = secKillService.executeSeckill(id,phone,md5);
logger.info("result: {}",seckillExecution);
} catch (RepeatKillException | SeckillCloseException e) {
logger.error(e.getMessage());
}
} else {
logger.warn("exposer={}",exposer);
}
}
}
本文同步发布于orzlinux.cn
|