美文网首页我爱编程JavaSE
基于SSM实现高并发秒杀Web项目(二)

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

作者: 熊猫读书营 | 来源:发表于2018-06-10 11:47 被阅读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());
            }
        }
    }
    

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

    相关文章

      网友评论

        本文标题:基于SSM实现高并发秒杀Web项目(二)

        本文链接:https://www.haomeiwen.com/subject/aiocjftx.html