SSM架构之高并发秒杀之Service详解

作者: 慕凌峰 | 来源:发表于2017-02-28 16:24 被阅读1089次

    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<Seckill>
         */
        public List<Seckill> 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<Seckill>
         */
        public List<Seckill> 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 功能详解
    Spring-IOC 业务依赖 使用Spring-IOC的原因 Spring-IOC的使用方式 本项目IOC使用方式

    1、Service 配置

    • spring-service.xml
    <?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="org.seckill.service"/>
    
        <!-- 配置Spring的声明式事务管理器 -->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <!-- 注入数据库的连接池 -->
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!-- 配置基于注解的声明式事务,默认使用注解来管理事务行为 -->
        <tx:annotation-driven transaction-manager="transactionManager"/>
    </beans>
    

    2、Spring 声明式事务

    传统数据库操作

    Spring来自动的管理事务的开启、提交、回滚,这种方式叫做声明式事务

    声明式事务的使用方式

    推荐使用第三种

    Spring 声明式事务 Spring 声明式事务回滚的的条件

    1)声明式事务的配置

    • 配置声明式事务的管理器

      • 在Spring相关的配置文件中配置声明式事务管理器
      • @Transactional 是Spring唯一的声明式事务注入标签
    • 使用注解控制事务方法的优点

      • 1、开发团队打成一个约定,明确标注事务方法的编程风格
      • 2、保证食物方法的执行时间尽可能短,不要穿插其他的网络操作,RPC/HTTP请求等,如果需要剥离到事务方法外部
      • 3、不是所有的方法都是需要声明式事务的,如:只有一条修改操作、只读操作不需要事务控制
    • logback.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <configuration debug="true">
    
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <!-- 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>
    

    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<Seckill> 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());
            }
        }
    }
    

    相关文章

      网友评论

      本文标题:SSM架构之高并发秒杀之Service详解

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