Java高并发之秒杀系统Service层开发

基于Spring整合的Java高并发之秒杀系统Service层开发。

一、秒杀接口设计

异常抛出

每个项目设计过程中都可能出现异常,所以从设计阶段就要考虑异常的处理问题。

在秒杀项目的设计中,异常分为三类:

  • 秒杀关闭异常(SeckillCloseException)
  • 重复秒杀异常(RepeatkillException)
  • 编译期异常转化的运行期异常的秒杀异常(SeckillException)

其中,秒杀关闭异常和重复秒杀异常继承秒杀异常。

秒杀异常继承运行期异常。

暴露秒杀接口

获取秒杀商品的抢购地址,接口防刷,提高代码重用率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Data
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;
}
}

秒杀接口设计

定义秒杀接口SeckillService。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface SeckillService {

/**
* 查询所有的秒杀记录
*
* @return
*/
List<Seckill> getSeckillList();

/**
* 根据ID查询单个秒杀记录
*
* @param seckillId
* @return
*/
Seckill getSeckillById(long seckillId);

/**
* 秒杀开启时输出秒杀地址
* 否则输出系统时和秒杀时间。
*
* @param seckillId
*/
Exposer exportSeckillUrl(long seckillId);

/**
* 执行秒杀操作
*
* @param seckillId
* @param userPhone
* @param md5
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException;
}

二、秒杀接口的实现

实现秒杀接口的方法,命名规范位接口名称+Impl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
实现秒杀接口的方法:
* 查询秒杀商品的列表
* 根据ID查询秒杀
* 暴露秒杀接口
* 执行秒杀操作
@Service
public class SeckillServiceImpl implements SeckillService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
//注入Service依赖
@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;

private final String slat = "#$^&)%^%_!@"; //用户混淆的盐值

@Override
public List<Seckill> getSeckillList() {
return seckillDao.queryAll(0, 4);
}

@Override
public Seckill getSeckillById(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);
}

private String getMD5(long seckillId) {
String base = seckillId + "/" + slat;
//生成MD5码
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}

@Override
@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 updateCount = seckillDao.reduceNumber(seckillId, nowTime);
if (updateCount <= 0) {
//没有更新记录,意味着秒杀结束
throw new SeckillCloseException("seckill closed");
} else {
//记录购买行为
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
//唯一的验证: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 e) {
//首先抛出运行期异常
throw e;
} catch (Exception e) {
logger.error(e.getMessage());
//将编译器异常转化位运行期异常
throw new SeckillException("seckill inner error:" + e.getMessage());
}
}
}

封装秒杀状态

执行秒杀后,将用户的秒杀状态封装到枚举类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@AllArgsConstructor
@Getter
public enum SeckillStatEnum {
SUCCESS(1, "秒杀成功"),
END(0, "秒杀结束"),
REPEAT_KILL(-1, "重复秒杀"),
INNER_ERROR(-2, "数据异常"),
DATA_REWRITE(-3, "数据篡改");

private int state;
private String stateInfo;

public static SeckillStatEnum stateOf(int index) {
for (SeckillStatEnum state : values()) {
if (state.getState() == index) {
return state;
}
}
return null;
}

}

三、Spring托管Service实现类

spring-service.xml

配置文件扫描service包下的注解(@Component,@Service,@Dao,@Controller),并自动注入到IoC容器中。

这里扫描@Service注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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="szy.seckill.service"/>
<!--配置事务管理器-->
<bean id="transactional" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--
配置基于注解的声明式事务
默认使用注解管理事务行为
-->
<tx:annotation-driven transaction-manager="transactional"/>
</beans>

四、DAO层单元测试

在IDEA中选中需要单元测试的类按下Ctrl+Shift+T,创建测试类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@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() {
logger.info("list={}", seckillService.getSeckillList());
}

@Test
public void getSeckillById() {
logger.info("seckill={}", seckillService.getSeckillById(1000));
}

//测试代码完整逻辑,注意可重复执行
@Test
public void exportLogic() {
long id = 1001L;
Exposer exposer = seckillService.exportSeckillUrl(id);
if (exposer.isExposed()) {
long userPhone = 12345678910L;
String md5 = exposer.getMd5();
try {
seckillService.executeSeckill(id, userPhone, md5);
} catch (RepeatKillException | SeckillCloseException e) {
logger.error(e.getMessage());
}
} else {
logger.warn("exposer={}", exposer);
}

}

}