我爱编程JavaSE

基于SSM实现高并发秒杀Web项目(二)

2018-06-10  本文已影响13人  熊猫读书营

这一部分是基于SSM实现高并发秒杀业务的第二节,主要介绍秒杀业务Service层的设计和实现,基于Spring托管Service实现类,并使用了Spring声明式事务。

源码可以在微信公众号【程序员修炼营】中获取哦,文末有福利

一、秒杀Service层接口设计

①准备工作:
新建service、dto、exception三个包。
dto:数据传输层,存放一些表示数据的类型。dto和entity较像,entity是业务(数据库表)的封装,dto是关注web和service之间的数据传递,此外dto层还包括了json格式的封装类,以便封装json数据。

exception:spring声明式事务只接收RuntimeException(运行期异常),只对运行期异常进行回滚。

②Service层接口设计
service/SeckillService.java 秒杀接口包含4个方法:查询全部秒杀记录、查询单个秒杀记录、暴露秒杀地址、执行秒杀。

配合接口中的方法,要同时设计好dto类以及异常类。

具体代码及注释如下:
service/SecKillService

public interface SecKillService {

    //查询全部的秒杀记录
    List<Seckill> getSeckillList();

    //查询单个秒杀记录
    Seckill getSecKillById(long seckillId);

    //在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间,是为了防止秒杀开启前就通过接口提前进行秒杀
    Exposer exporySecKillUrl(long seckillId);

    //执行秒杀操作,有可能失败,有可能成功,所以要抛出我们允许的异常
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SecKillException, RepeatKillException, SecKillCloseException;

}

dto/Exposer

//暴露秒杀地址
public class Exposer {

    private long seckillId;

    //是否开启秒杀
    private boolean exposed;

    //md5加密方式
    private String md5;

    //系统当前时间(毫秒)
    private long now;

    //开启时间
    private long start;

    //结束时间
    private long end;
}

后面自行补充相应的构造方法及getter()、setter()方法。

dto/SeckillExecution

//封装秒杀执行后的结果
public class SeckillExecution {

    private long seckillId;

    //秒杀执行结果状态
    private int state;

    //状态表示
    private String stateInfo;

    //秒杀成功对象
   private SuccessKilled successKilled;

    public SeckillExecution(long seckillId, SeckillStateEnum stateEnum, SuccessKilled successKilled) {
        this.seckillId = seckillId;
        this.state = stateEnum.getState();
        this.stateInfo = stateEnum.getStateInfo();
        this.successKilled = successKilled;
    }

    public SeckillExecution(long seckillId, SeckillStateEnum stateEnum) {
        this.seckillId = seckillId;
        this.state = stateEnum.getState();
        this.stateInfo = stateEnum.getStateInfo();
    }
}

exception/SecKillException

//秒杀相关业务异常
public class SecKillException extends RuntimeException{

    public SecKillException(String message){
        super(message);
    }

    public SecKillException(String message, Throwable cause){
        super(message, cause);
    }

}

exception/RepeatKillException

//重复秒杀异常
public class RepeatKillException extends SecKillException{
    public RepeatKillException(String message){
        super(message);
    }

    public RepeatKillException(String message, Throwable cause){
        super(message, cause);
    }
}

exception/SecKillCloseException

//秒杀关闭异常
public class SecKillCloseException extends SecKillException{

    public SecKillCloseException(String message){
        super(message);
    }

    public SecKillCloseException(String message, Throwable cause){
        super(message);
    }

}

二、秒杀Service层接口实现

service包下新建接口实现类SecKillServiceImpl

public class SecKillServiceImpl implements SecKillService{

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private SeckillDao seckillDao;

    private SuccessKillDao successKillDao;

    //md5盐值字符串,用于混淆md5
    private final String slat = "asfsafse345!$#&*#)(uiashgfdq12";

    @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;
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }

    @Override
    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 is closed");
            } else {
                //记录购买行为
                int insertCount = successKillDao.insertSuccessKilled(seckillId, userPhone);
                if (insertCount <= 0){//重复秒杀了
                    throw new RepeatKillException("seckill repeated");
                } else {//秒杀成功
                    SuccessKilled successKilled = successKillDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successKilled);
                }
            }
        } catch (SecKillCloseException e1){
            throw e1;
        } catch (RepeatKillException e2){
            throw e2;
        }catch(Exception e){
            logger.error(e.getMessage(), e);
            //所有编译期异常转化为运行期异常,Spring声明式事务会进行rollback回滚
            throw new SecKillException("seckill inner error:" + e.getMessage());
        }
    }
}

用常量表示数据字典,新建枚举类enums/SeckillStateEnum 存放状态,状态信息对应的常量。

//使用枚举表示常量数据字典
public enum SeckillStateEnum {

    SUCCESS(1,"秒杀成功"),
    END(0,"秒杀结束"),
    REPEAT_KILL(-1,"重复秒杀"),
    INNER_ERROR(-2,"系统异常"),
    DATE_REWRITE(-3,"数据篡改");

    private int state;
    private String stateInfo;

    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;
    }

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

}

三、基于Spring托管Service实现类

①Spring通过IOC来管理依赖,业务对象的简单依赖图如下:

②使用SpringIOC有下面几点好处:

  1. 对象创建可以统一进行管理,无需new操作
  2. 规范的生命周期管理,init()、destory()等方法可以自由增加业务逻辑
  3. 灵活的依赖注入
  4. 一致的获取对象方式,可以很方便的从容器中取出对象
③有关SpringIOC的三种注入方式: ④本项目中使用IOC方式:

⑤进行依赖配置
在resources/spring 目录下新建spring-service.xml配置文件,以便向IOC装配service层的bean。

<?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="service"/>
    
</beans>

SecKillServiceImpl实现类上加上@Service注解,会将类自动转换为bean并注入到IOC容器中。
SeckillDao属性和SuccessKillDao属性上加上@Autowired注解,进行依赖注入。

四、使用Spring声明式事务

①Spring声明式事务有两种常见的用法:

  1. tx:advice+aop命名空间,一次配置永久生效。
  2. @Transcational,通过注解标注需要事务的方法。

②不是所有方法都需要事务,比如只有一条修改操作、只读操作就不需要事务控制,所以这里建议用注解控制(@Transcational)的方式标注需要事务的方法,而不建议采用tx-advice+aop命名空间对所有方法都标注事务。

④标注了@Transcational方法,开启一个事务,在最后return或者throw RuntimeException时才commit或rollback

⑤声明式事务配置
在spring-service.xml配置事务管理器,配置基于注解的声明式事务。

    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据库连接池-->
        <property name="dataSource" ref="dataSource"/>

    </bean>

    <!--配置基于注解的声明式事务
    默认使用注解来管理事务行为-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

⑥SecKillServiceImpl实现类中的executeSeckill方法上加上@Transactional注解,进行事务管理。

五、集成测试Service逻辑

①使用快捷键shift+ctrl+t快速生成SecKillServiceI接口的测试方法
② slf4j接口的实现类logback配置:在测试类中声明logger变量,然后新建resources/logback.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are  by default assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
        <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>

③编写测试方法,逐一进行测试

//Spring启动时加载spring容器
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit spring的配置文件,完成bean的注入
@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> seckills = secKillService.getSeckillList();
        logger.info("list = {}", seckills);
    }

    @Test
    public void getSecKillById() {
        long id = 1000L;
        Seckill seckill = secKillService.getSecKillById(id);
        logger.info("seckill = {}", seckill);
    }

    //集成测试代码完整逻辑,注意可重复执行,注意测试的业务覆盖完整性。
    @Test
    public void seckillLogic() throws Exception {
        long id = 1001L;
        Exposer exposer = secKillService.exportSecKillUrl(id);
        if (exposer.isExposed()) {
            logger.info("exposer = {}", exposer);
            long userPhone = 13054477731L;
            String md5 = exposer.getMd5();
            try {
                SeckillExecution seckillExecution = secKillService.executeSeckill(id, userPhone, md5);
                logger.info("result = {}", seckillExecution);
            } catch (SecKillCloseException e) {
                logger.error(e.getMessage());
            } catch (RepeatKillException e) {
                logger.error(e.getMessage());
            }

        }
        else {
            //秒杀未开启
            logger.warn("exposer= {}", exposer);
        }
    }

    @Test
    public void executeSeckillProcedure() {
        long seckillId = 1000L;
        long phone = 1365422212L;
        Exposer exposer = secKillService.exportSecKillUrl(seckillId);
        if (exposer.isExposed()) {
            String md5 = exposer.getMd5();
            SeckillExecution execution = secKillService.executeSeckill(seckillId, phone, md5);
            logger.info(execution.getStateInfo());
        }
    }
}

每个测试方法均验证通过,成功!

上一篇下一篇

猜你喜欢

热点阅读