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

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 功能详解
SSM架构之高并发秒杀之Service详解_第1张图片
Spring-IOC
SSM架构之高并发秒杀之Service详解_第2张图片
业务依赖
SSM架构之高并发秒杀之Service详解_第3张图片
使用Spring-IOC的原因
SSM架构之高并发秒杀之Service详解_第4张图片
Spring-IOC的使用方式
SSM架构之高并发秒杀之Service详解_第5张图片
本项目IOC使用方式

1、Service 配置

  • spring-service.xml


    
    

    
    
        
        
    

    
    

2、Spring 声明式事务

SSM架构之高并发秒杀之Service详解_第6张图片
传统数据库操作

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

SSM架构之高并发秒杀之Service详解_第7张图片
声明式事务的使用方式

推荐使用第三种

SSM架构之高并发秒杀之Service详解_第8张图片
Spring 声明式事务
SSM架构之高并发秒杀之Service详解_第9张图片
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*

你可能感兴趣的:(SSM架构之高并发秒杀之Service详解)