一、Service层的设计
(1)编写service接口
/**
* 秒杀的业务接口
* @author liu
*/
public interface SeckillService {
/**
* 查询所有秒杀的商品
* @return
*/
List getSeckillList();
/**
* 查询单个秒杀的商品
* @param seckillId
* @return
*/
Seckill getById(long seckillId);
/**
* 到了秒杀的时间,暴露秒杀的地址
* @param seckillId
* @return 秒杀的地址
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 执行秒杀,验证用户手机号
* @param seckillId
* @param userPhone
* @param MD5
* @return 秒杀的结果
* @throws SeckillException
* @throws SeckillRepeatException
* @throws SeckillCloseException
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String MD5)
throws SeckillException, SeckillRepeatException, SeckillCloseException;
}
可以看到定义了四个方法,重点讲讲后两个方法。
暴露秒杀地址:可以看到返回值是Exposer,该类主要定义了一些字段,用于判断是否开启秒杀地址。
执行秒杀:该类是核心方法,返回值是SeckillExecution,SeckillExecution类封装的是秒杀的结果(包括成功秒杀和秒杀失败),在执行秒杀的方法中,如果秒杀成功,则减少库存,增加秒杀记录,并封装秒杀结果,这里要使用spring事务控制。
(2)编写service接口的实现类
@Service
public class SeckillServlceImpl implements SeckillService {
// 获取日志对象
private Logger logger = LoggerFactory.getLogger(this.getClass());
// 秒杀dao的对象
@Autowired
private SeckillDao sd;
// 秒杀成功dao的对象
@Autowired
private SuccessKilledDao skd;
// MD5盐值字符串,用于混淆MD5
private final String salt = "hjhad892998^456#$(@8K";
@Override
/**
* 获取所有的秒杀商品
*/
public List getSeckillList() {
return sd.queryAll(0, 4);
}
@Override
/**
* 根据id获取秒杀商品
*/
public Seckill getById(long seckillId) {
return sd.queryById(seckillId);
}
@Override
/**
* 暴露秒杀地址
*/
public Exposer exportSeckillUrl(long seckillId) {
Seckill seckill = sd.queryById(seckillId);
// 如果要秒杀的商品不存在
if(seckill == null) {
// 调用相关的构造函数
return new Exposer(false, seckillId);
}
// 获取当前的时间
Date date = new Date();
// 获取秒杀开启时间
Date startTime = seckill.getStartTime();
// 获取秒杀结束时间
Date endTime = seckill.getEndTime();
// 如果当前时间大于秒杀结束时间或小于秒杀开始时间,则不开启秒杀接口
if(date.getTime() < startTime.getTime() ||
date.getTime() > endTime.getTime()) {
return new Exposer(false, seckillId, date.getTime(), startTime.getTime(), endTime.getTime());
}
// 转化特定字符串的过程,不可逆
String MD5 = getMD5(seckillId);
return new Exposer(true, MD5, seckillId);
}
/**
* 获取MD5
* @return
*/
private String getMD5(long seckillId) {
String base = seckillId + "/" + salt;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
@Override
/**
* 执行秒杀
* 使用注解控制事务的优点
* 1 开发团队达成一致约定,明确标注事务方法的编程风格
* 2 保证事务方法的执行时间尽可能短,不要穿插其他网络请求比如RPC/HTTP等,如果要,请剥离到事务方法外部
* 3 不是所有方法都需要事务,比如只有一条修改操作,只读操作不需要事务控制
*/
@Transactional
public SeckillExecution executeSeckill(long seckillId, long userPhone, String MD5)
throws SeckillException, SeckillRepeatException, SeckillCloseException {
if(MD5 == null || !MD5.equals(getMD5(seckillId))) {
throw new SeckillException("秒杀数据被重写");
}
// 执行秒杀的逻辑:减库存+增加购买记录
Date now = new Date();
try {
// 减库存
int updateCount = sd.reduceNumber(seckillId, now);
// 如果没有更新记录,则说明秒杀结束
if(updateCount <= 0) {
throw new SeckillCloseException("秒杀结束");
} else {
// 插入秒杀成功的记录
int insertCount = skd.insertSuccessKilled(seckillId, userPhone);
// 如果插入的记录小于0,说明重复插入了
if(insertCount <= 0) {
throw new SeckillRepeatException("重复秒杀");
} else {
// 秒杀成功,查询出插入的秒杀成功的记录
SuccessKilled sk = skd.queryByIdWithSeckill(seckillId, userPhone);
// 封装成功秒杀结果返回
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, sk);
}
}
} catch(SeckillCloseException e1) {
throw e1;
} catch(SeckillRepeatException e2) {
throw e2;
} catch(SeckillException e) {
logger.error(e.getMessage(), e);
throw new SeckillException("秒杀错误:" + e.getMessage());
}
}
}
主要注意暴露秒杀接口和执行秒杀这两个方法,清楚里面的逻辑。同时这里使用了MD5加密,防止同一用户重复秒杀。
同时注意执行秒杀的这个方法中,有三个自定义的异常,都继承了runtimeException,之所以继承runtimeException,是因为运行时异常才能引起spring的事务回滚。
(3)编写配置文件
这里配置了事务管理器,其他的都使用注解。
(4)编写测试类进行测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"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 testGetSeckillList() {
List list = seckillService.getSeckillList();
// 输出时会把list放入占位符{}中
logger.info("list = {}" + list);
}
@Test
public void testGetById() {
long id = 1000L;
Seckill seckill = seckillService.getById(id);
logger.info("seckill = {}" + seckill);
}
@Test
/**
* exposer = {}Exposer [exposed=true, MD5=073ece008a409c7bc971949f1b183fe9, seckillId=1000, now=0, start=0, end=0]
*/
public void testSeckillLogic() {
long id = 1000L;
Exposer exposer = seckillService.exportSeckillUrl(id);
// 如果秒杀已经开始了
if(exposer.isExposed()) {
long userPhone = 18970718197L;
String md5 = exposer.getMD5();
try {
SeckillExecution se = seckillService.executeSeckill(id, userPhone, md5);
logger.info("se = {}" + se);
} catch(SeckillCloseException e1) {
throw e1;
} catch(SeckillRepeatException e2) {
throw e2;
}
} else {
// 秒杀未开始,打印警告
logger.warn("exposer = {}" + exposer);
}
}
}
二、遇到的错误
(1)如下
com.mchange.v2.resourcepool.TimeoutException: A client timed out while waiting to acquire a resource from com.mchange.v2.resourcepool.BasicResourcePool@4c3487 -- timeout at awaitAvailable()
出现该错误的原因,并不是代码写出了,是因为我配置c3p0私有属性的时候,把一个属性的值配置的太小了,如下
把数值改大后就解决了,这个视情况而定,因为我的电脑太卡了。
三、总结
在设计一个项目的时候,没写完一个阶段的代码,就应该进行单元测试,不要在全部写完后才进行测试,这样debug会很困难,大神除外。
在service层,你应该考虑如何设计一个优雅的接口,去实现相应的功能。