SSM架构之高并发秒杀之Dao http://www.jianshu.com/p/15ccf298d486
一、Service 层基本项目项目开发
1、enums
枚举常量
- SeckillStateEnum.java
使用枚举表示我们的常量数据字典
package org.seckill.enums;
/**
* 使用枚举表示我们的常量数据字典
* Created by wangxf on 2017/2/25.
*/
public enum SeckillStateEnum {
SUCCESS(1,"秒杀成功"),
END(0,"秒杀结束"),
REPEAT_KILL(-1,"重复秒杀"),
INNER_ERROR(-2,"系统异常"),
DATA_REWRITE(-3,"数据篡改");
private int state;
private String stateInfo;
public static SeckillStateEnum stateOf(int index){
for (SeckillStateEnum stateEnum : values()) {
if (stateEnum.getState() == index) {
return stateEnum;
}
}
return null;
}
SeckillStateEnum(int state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
}
public int getState() {
return state;
}
public String getStateInfo() {
return stateInfo;
}
public void setState(int state) {
this.state = state;
}
public void setStateInfo(String stateInfo) {
this.stateInfo = stateInfo;
}
}
2、dto
- Exposer.java
用于暴露秒杀地址的DTO
package org.seckill.dto;
/**
* 用于暴露秒杀地址的DTO
* Created by wangxf on 2017/2/24.
*/
public class Exposer {
private boolean exposed; // 用户判断秒杀接口是否开启
private String md5; // 一种加密机制
private long seckillId; // 秒杀id
private long nowTime; // 系统的当前时间(毫秒)
private long startTime; // 秒杀的开启时间(毫秒)
private long endTime; // 秒杀的结束时间(毫秒)
public Exposer() {
}
public Exposer(boolean exposed, String md5, long seckillId) {
this.exposed = exposed;
this.md5 = md5;
this.seckillId = seckillId;
}
public Exposer(boolean exposed, long nowTime, long startTime, long endTime) {
this.exposed = exposed;
this.nowTime = nowTime;
this.startTime = startTime;
this.endTime = endTime;
}
public Exposer(boolean exposed, long seckillId) {
this.exposed = exposed;
this.seckillId = seckillId;
}
public Exposer(boolean exposed, long seckillId, long nowTime, long startTime, long endTime) {
this.exposed = exposed;
this.seckillId = seckillId;
this.nowTime = nowTime;
this.startTime = startTime;
this.endTime = endTime;
}
public boolean isExposed() {
return exposed;
}
public String getMd5() {
return md5;
}
public long getSeckillId() {
return seckillId;
}
public long getNowTime() {
return nowTime;
}
public long getStartTime() {
return startTime;
}
public long getEndTime() {
return endTime;
}
public void setExposed(boolean exposed) {
this.exposed = exposed;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public void setNowTime(long nowTime) {
this.nowTime = nowTime;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
public void setEndTime(long endTime) {
this.endTime = endTime;
}
@Override
public String toString() {
return "Exposer{" +
"exposed=" + exposed +
", md5='" + md5 + '\'' +
", seckillId=" + seckillId +
", nowTime=" + nowTime +
", startTime=" + startTime +
", endTime=" + endTime +
'}';
}
}
- SeckillExcution.java
封装执行秒杀后的数据
package org.seckill.dto;
import org.seckill.bean.SuccessKilled;
import org.seckill.enums.SeckillStateEnum;
/**
* 封装执行秒杀后的数据
* Created by wangxf on 2017/2/24.
*/
public class SeckillExcution {
private long seckillId; // 秒杀信息id
private int state; // 秒杀执行结构状态
private String stateInfo; // 执行结果状态标识
private SuccessKilled successKilled; // 秒杀成功的对象
public SeckillExcution() {
}
public SeckillExcution(long seckillId, SeckillStateEnum stateEnum) {
this.seckillId = seckillId;
this.state = stateEnum.getState();
this.stateInfo = stateEnum.getStateInfo();
}
public SeckillExcution(long seckillId, SeckillStateEnum stateEnum, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = stateEnum.getState();
this.stateInfo = stateEnum.getStateInfo();
this.successKilled = successKilled;
}
public long getSeckillId() {
return seckillId;
}
public int getState() {
return state;
}
public String getStateInfo() {
return stateInfo;
}
public SuccessKilled getSuccessKilled() {
return successKilled;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public void setState(int state) {
this.state = state;
}
public void setStateInfo(String stateInfo) {
this.stateInfo = stateInfo;
}
public void setSuccessKilled(SuccessKilled successKilled) {
this.successKilled = successKilled;
}
@Override
public String toString() {
return "SeckillExcution{" +
"seckillId=" + seckillId +
", state=" + state +
", stateInfo='" + stateInfo + '\'' +
", successKilled=" + successKilled +
'}';
}
}
2、exception
异常定义
- SeckillException.java
所有秒杀业务相关的异常(运行时异常)
package org.seckill.exception;
/**
* 所有秒杀业务相关的异常(运行时异常)
* Created by wangxf on 2017/2/24.
*/
public class SeckillException extends RuntimeException{
public SeckillException() {
}
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
- SeckillCloseException.java
秒杀关闭时异常(运行时异常)
package org.seckill.exception;
/**
* 秒杀关闭时异常(运行时异常)
* Created by wangxf on 2017/2/24.
*/
public class SeckillCloseException extends SeckillException{
public SeckillCloseException() {
}
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
- RepeatKillException.java
重复秒杀异常(运行时异常)
package org.seckill.exception;
/**
* 重复秒杀异常(运行时异常)
* Created by wangxf on 2017/2/24.
*/
public class RepeatKillException extends SeckillException{
public RepeatKillException() {
}
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
3、Service
- ISeckillService.java
业务接口,站在用户的角度设计开发接口, 三个方面:方法定义粒度、参数、返回类型/异常
package org.seckill.service.interfaces;
import org.seckill.bean.Seckill;
import org.seckill.bean.SuccessKilled;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExcution;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
import java.util.List;
/**
* 业务接口,站在用户的角度设计开发接口
* 三个方面:方法定义粒度、参数、返回类型/异常
* Created by wangxf on 2017/2/23.
*/
public interface ISeckillService {
/**
* 查询所有的秒杀记录
* @return List
*/
public List selectSeckillList();
/**
* 通过 id 精确查询秒杀记录信息
* @param seckillId 秒杀信息id
* @return Seckill
*/
public Seckill selectSeckillById(long seckillId);
/**
* 秒杀开启时输出秒接口地址
* 否则,输出系统时间或者秒杀时间
* @param seckillId 秒杀信息id
* @return Exposer
*/
public Exposer exportSeckillUrlException(long seckillId);
/**
* 用户执行秒杀操作
* @param seckillId 秒杀信息id
* @param userPhone 用户手机号码
* @param md5 密文
* @return SeckillExcution
*/
public SeckillExcution excuteSeckill(long seckillId, long userPhone, String md5) throws SeckillException,RepeatKillException,SeckillCloseException;
}
- SeckillServiceImpl.java
业务接口,站在用户的角度设计开发接口,三个方面:方法定义粒度、参数、返回类型/异常
package org.seckill.service.impl;
import org.seckill.bean.Seckill;
import org.seckill.bean.SuccessKilled;
import org.seckill.dao.interfaces.ISeckillDao;
import org.seckill.dao.interfaces.ISuccessKilledDao;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExcution;
import org.seckill.enums.SeckillStateEnum;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
import org.seckill.service.interfaces.ISeckillService;
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.List;
/**
* 业务接口,站在用户的角度设计开发接口
* 三个方面:方法定义粒度、参数、返回类型/异常
*
* Spring 的类注解主要有:@Component 所有的注解、@Controller @Service @Dao
* * Created by wangxf on 2017/2/23.
*/
@Service
public class SeckillServiceImpl implements ISeckillService{
private Logger logger = LoggerFactory.getLogger(this.getClass()); // 日志对象
// 注入 service 依赖
// @Autowired 自动加载依赖 @Resource @Inject 注入一些规范等
@Autowired
private ISeckillDao seckillDao; // dao层秒杀对象
@Autowired
private ISuccessKilledDao successKilledDao; // Dao秒杀成功后对象
// MD5 腌制字符串,用于混洗 MD5
private final String slat = "diasj29er2ur734tuei89u34efdfi30q7u5834tdphf056=-251758";
/**
* 查询所有的秒杀记录
* @return List
*/
public List selectSeckillList() {
return seckillDao.selectProductAll(0,4);
}
/**
* 通过 id 精确查询秒杀记录信息
* @param seckillId 秒杀信息id
* @return Seckill
*/
public Seckill selectSeckillById(long seckillId) {
return seckillDao.selectProductById(seckillId);
}
/**
* 秒杀开启时输出秒接口地址
* 否则,输出系统时间或者秒杀时间
* @param seckillId 秒杀信息id
* @return Exposer
*/
public Exposer exportSeckillUrlException(long seckillId) {
// 根据seckillId查询秒杀信息
Seckill seckill = seckillDao.selectProductById(seckillId);
// 判断秒杀对象是否为空,如果为空,返回 Exposer 对象为:false
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());
}
// 生成 md5, md5 就是转换特定字符串的过程,这个过程是不可逆的
String md5 = getMd5(seckillId);
return new Exposer(true, md5, seckillId);
}
/**
* 用户执行秒杀操作,正真秒杀操作的实现
* @param seckillId 秒杀信息id
* @param userPhone 用户手机号码
* @param md5 密文
* @return
* @throws SeckillException
* @throws RepeatKillException
* @throws SeckillCloseException
*/
/*
* 使用注解控制事务方法的优点
* 1、开发团队打成一个约定,明确标注事务方法的编程风格
* 2、保证食物方法的执行时间尽可能短,不要穿插其他的网络操作,RPC/HTTP请求等,如果需要剥离到事务方法外部
* 3、不是所有的方法都是需要声明式事务的,如:只有一条修改操作、只读操作不需要事务控制
*/
@Transactional
public SeckillExcution excuteSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
// 这里try ... catch 是为了:防止除我们自定义之外的异常
try {
// 判断用户传入的 md5 是否与生成的md5 相匹配
// 当不匹配时,抛出异常
if (null == md5 || !md5.equals(getMd5(seckillId))) {
throw new SeckillException("秒杀数据有误:seckill data rewrite!");
}
// 秒杀的业务逻辑 : 减库存 + 记录购买行为
// 获取当前时间
Date nowTime = new Date();
// 减库存
int updateProductNumber = seckillDao.updateProductNumber(seckillId, nowTime);
// 判断修改数据库行数,如果 小于等于 0 :说明修改失败
if (updateProductNumber <= 0) {
throw new SeckillCloseException("秒杀还未开始或已经结束或者库存不足!");
} else {
// 记录购买行为 主键为:seckillId + userPhone 防止重复秒杀
int insertSuccessKilled = successKilledDao.insertSuccessKilled(seckillId, userPhone);
// 如果未能成功插入,即 返回结果为0时,表示主键冲突,即已经秒杀过了
if (insertSuccessKilled <= 0) {
throw new RepeatKillException("不能够重复秒杀!");
} else {
// 秒杀成功,返回秒杀记录对象
SuccessKilled successKilled = successKilledDao.selectByIdWithSeckill(seckillId, userPhone);
if (null != successKilled) {
return new SeckillExcution(seckillId, SeckillStateEnum.SUCCESS,successKilled);
}
}
}
// 再抛出异常时,为了避免将我们自定义的异常转换成其他异常,顾,先抛出我们自己的异常
} catch (SeckillCloseException e1) {
throw e1;
} catch (RepeatKillException e2) {
throw e2;
} catch (Exception e) {
// 记录日志
logger.error(e.getMessage(), e);
// 将所有的编译时异常转变成运行时异常
throw new SeckillException("秒杀时异常:" + e.getMessage());
}
return null;
}
/**
* 获取 md5 密文信息
* @param seckillId 秒杀产品id
* @return String
*/
private String getMd5(long seckillId) {
String base = seckillId + "/" + slat;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
}
二、基于Spring的Service依赖
- Spring IOC 功能详解
1、Service 配置
- spring-service.xml
2、Spring 声明式事务
Spring来自动的管理事务的开启、提交、回滚,这种方式叫做声明式事务
推荐使用第三种
1)声明式事务的配置
-
配置声明式事务的管理器
- 在Spring相关的配置文件中配置声明式事务管理器
- @Transactional 是Spring唯一的声明式事务注入标签
-
使用注解控制事务方法的优点
- 1、开发团队打成一个约定,明确标注事务方法的编程风格
- 2、保证食物方法的执行时间尽可能短,不要穿插其他的网络操作,RPC/HTTP请求等,如果需要剥离到事务方法外部
- 3、不是所有的方法都是需要声明式事务的,如:只有一条修改操作、只读操作不需要事务控制
logback.xml
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
3、测试类
- ISeckillServiceTest.java
package org.seckill.service.interfaces;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.bean.Seckill;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExcution;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
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.*;
/**
* ISeckillService 接口的测试类
* Spring 和 junit 整合
* 目的:视为了让 junit 在启动时加载 Spring IOC 容器
* 原因:因为 Dao 接口的实现是由 Spring 完成的
*
* 实现:通过 JUnit 的@RunWith(SpringJUnit4ClassRunner.class) 接口来加载Spring 的SpringJUnit4ClassRunner
* 在加载时,用Spring 的ContextConfiguration来加载验证 MyBatis 与 Spring 的整合文件
* spring-test、junit{}
* Created by wangxf on 2017/2/22.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/spring-service.xml","classpath:spring/spring-dao.xml"})
public class ISeckillServiceTest {
// 日志的定义
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private ISeckillService seckillService;
@Test
public void selectSeckillList() throws Exception {
List seckillList = seckillService.selectSeckillList();
logger.info("list={}" + seckillList);
}
@Test
public void selectSeckillById() throws Exception {
long seckillId = 1000L;
Seckill seckill = seckillService.selectSeckillById(seckillId);
logger.info("seckill={}" + seckill);
}
/**
* exportSeckillUrlException + excuteSeckill 的联合测试
* @throws Exception
*/
@Test
public void exportSeckillUrlException() throws Exception {
long seckillId = 1000L;
Exposer exposer = seckillService.exportSeckillUrlException(seckillId);
// 判断秒杀接口是否开启
if (exposer.isExposed()) {
logger.info("exposer={ }" + exposer);
long userPhone = 18779118283L;
String md5 = exposer.getMd5();
try {
SeckillExcution seckillExcution = seckillService.excuteSeckill(seckillId, userPhone, md5);
logger.info("seckillExcution ={}" + seckillExcution);
} catch (SeckillCloseException e) {
logger.error(e.getMessage());
} catch (RepeatKillException e1) {
logger.error(e1.getMessage());
}
} else {
logger.warn("exposer={}" + exposer + "秒杀还外开启");
}
// md5 = 0460b09c6e8028d6b2620c1d75c34f4b
}
@Test
public void excuteSeckill() throws Exception {
long seckillId = 1000L;
long userPhone = 18779118283L;
String md5 = "0460b09c6e8028d6b2620c1d75c34f4b";
try {
SeckillExcution seckillExcution = seckillService.excuteSeckill(seckillId, userPhone, md5);
logger.info("seckillExcution ={}" + seckillExcution);
} catch (SeckillCloseException e) {
logger.error(e.getMessage());
} catch (RepeatKillException e1) {
logger.error(e1.getMessage());
}
}
}
- 项目参考:http://www.imooc.com/learn/631*