Java高并发秒杀——Service层

Service层:完成DAO层的拼接以及其他逻辑

Service层分析目录

  1、Service层接口设计与实现
  2、Spring装配Service实现类
  3、Spring声明式事务配置
  4、Junit单元测试Service层

一、Service层接口设计与实现

  1、创建业务接口:站在使用者的角度设计接口,三个方面:方法定义粒度,参数,返回类型(return 类型/异常)
package com.fehead.service;
	
	import com.fehead.bean.Seckill;
	import com.fehead.dto.Exposer;
	import com.fehead.dto.SeckillExecution;
	import com.fehead.exception.RepeatKillException;
	import com.fehead.exception.SeckillCloseException;
	import com.fehead.exception.SeckillException;
	
	import javax.swing.text.html.parser.Entity;
	import java.util.List;
	
	/**
	 * Created by xiaoaxiao on 2019/5/3
	 * Description:业务接口:站在“使用者”角度设计接口
	 *             三个方面:方法定义粒度,参数,返回类型(return 类型/异常)
	 */
	public interface SeckillService {
	
	    /**
	     * 查询所有秒杀记录
	     * @return
	     */
	    List<Seckill> getSeckillList();
	
	    /**
	     * 查询单个秒杀记录
	     * @param seckillId
	     * @return
	     */
	    Seckill getSeckillById(long seckillId);
	
	
	
	    /**
	     * 秒杀开启时输出秒杀接口地址,秒杀存在但未开启时输出系统时间和秒杀时间,秒杀不存在直接返回id就行
	     * ——这可以通过创建多个Exposer的构造器来实现
	     * 当web层或接口使用方调用该方法时,可以拿到Exposer的dto,可以看到一些返回的数据
	     * @param seckillId
	     * @return
	     */
	    Exposer exportSeckillUrl(long seckillId);
	
	    /**
	     * 执行秒杀操作
	     * @param seckillId
	     * @param userPhone
	     * @param md5
	     * @return
	     */
	    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
	        throws SeckillException, SeckillCloseException, RepeatKillException;
	    //抛出SeckillCloseException, RepeatKillException异常需要明确告诉用户或接口使用者,秒杀关闭异常/重复秒杀异常
	    //而抛出SeckillException异常只需要告诉用户秒杀存在异常,秒杀失败
	
	    /**
	     * 执行秒杀操作by 存储过程
	     * @param seckillId
	     * @param userPhone
	     * @param md5
	     * @return
	     */
	    SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5)
	            throws SeckillException, SeckillCloseException, RepeatKillException;
	}
    通过创建多个Exposer构造器实现秒杀的不同情况:1、秒杀开启时输出秒杀接口地址;2、秒杀存在但未开启时输出系统时间和秒杀时间;3、秒杀不存在时直接返回id
package com.fehead.dto;

/**
 * Created by xiaoaxiao on 2019/5/3
 * Description:暴露秒杀地址DTO
 *   DTO层:放与大部分业务不相关,方便Service层返回的数据的封装
 */
public class Exposer {

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

    //一种加密措施
    private String md5;

    //id
    private long seckillId;

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

    //秒杀开始时间
    private long start;

    //秒杀结束时间
    private long end;

    public Exposer(boolean exposed, String md5, long seckillId) {
        this.exposed = exposed;
        this.md5 = md5;
        this.seckillId = seckillId;
    }

    public Exposer(boolean exposed,long seckillId, long now, long start, long end) {
        this.exposed = exposed;
        this.seckillId = seckillId;
        this.now = now;
        this.start = start;
        this.end = end;
    }

    public Exposer(boolean exposed, long seckillId) {
        this.exposed = exposed;
        this.seckillId = seckillId;
    }

    public boolean isExposed() {
        return exposed;
    }

    public void setExposed(boolean exposed) {
        this.exposed = exposed;
    }

    public String getMd5() {
        return md5;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    public long getSeckillId() {
        return seckillId;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public long getNow() {
        return now;
    }

    public void setNow(long now) {
        this.now = now;
    }

    public long getStart() {
        return start;
    }

    public void setStart(long start) {
        this.start = start;
    }

    public long getEnd() {
        return end;
    }

    public void setEnd(long end) {
        this.end = end;
    }

    @Override
    public String toString() {
        return "Exposer{" +
                "exposed=" + exposed +
                ", md5='" + md5 + '\'' +
                ", seckillId=" + seckillId +
                ", now=" + now +
                ", start=" + start +
                ", end=" + end +
                '}';
    }
}
package com.fehead.dto;

	import com.fehead.bean.SuccessKilled;
	import com.fehead.enums.SeckillStatEnum;
	
	/**
	 * Created by xiaoaxiao on 2019/5/3
	 * Description:封装秒杀完成后的结果
	 */
	public class SeckillExecution {
	
	    private long seckillId;
	
	    private int state;
	
	    private String stateInfo;
	
	    private SuccessKilled successKilled;
	
	    public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) {
	        this.seckillId = seckillId;
	        this.state = statEnum.getState();
	        this.stateInfo = statEnum.getStateInfo();
	        this.successKilled = successKilled;
	    }
	
	    public SeckillExecution(long seckillId, SeckillStatEnum statEnum) {
	        this.seckillId = seckillId;
	        this.state = statEnum.getState();
	        this.stateInfo = statEnum.getStateInfo();
	    }
	
	    public long getSeckillId() {
	        return seckillId;
	    }
	
	    public void setSeckillId(long seckillId) {
	        this.seckillId = seckillId;
	    }
	
	    public int getState() {
	        return state;
	    }
	
	    public void setState(int state) {
	        this.state = state;
	    }
	
	    public String getStateInfo() {
	        return stateInfo;
	    }
	
	    public void setStateInfo(String stateInfo) {
	        this.stateInfo = stateInfo;
	    }
	
	    public SuccessKilled getSuccessKilled() {
	        return successKilled;
	    }
	
	    public void setSuccessKilled(SuccessKilled successKilled) {
	        this.successKilled = successKilled;
	    }
	
	    @Override
	    public String toString() {
	        return "SeckillExecution{" +
	                "seckillId=" + seckillId +
	                ", state=" + state +
	                ", stateInfo='" + stateInfo + '\'' +
	                ", successKilled=" + successKilled +
	                '}';
	    }
	}
package com.fehead.exception;
	
	/**
	 * Created by xiaoaxiao on 2019/5/3
	 * Description:秒杀相关业务异常,所以其他异常(重复异常/关闭异常)都是相关业务异常的子类
	 */
	public class SeckillException extends RuntimeException{
	
	    public SeckillException(String message) {
	        super(message);
	    }
	
	    public SeckillException(String message, Throwable cause) {
	        super(message, cause);
	    }
	}
package com.fehead.exception;
	
	/**
	 * Created by xiaoaxiao on 2019/5/3
	 * Description:重复秒杀异常(运行期异常)
	 */
	public class RepeatKillException extends SeckillException {
	
	    public RepeatKillException(String message) {
	        super(message);
	    }
	
	    public RepeatKillException(String message, Throwable cause) {
	        super(message, cause);
	    }
	}
package com.fehead.exception;
	
	/**
	 * Created by xiaoaxiao on 2019/5/3
	 * Description:
	 */
	public class SeckillCloseException extends SeckillException {
	
	    public SeckillCloseException(String message) {
	        super(message);
	    }
	
	    public SeckillCloseException(String message, Throwable cause) {
	        super(message, cause);
	    }
	}
package com.fehead.enums;
	
	/**
	 * Created by xiaoaxiao on 2019/5/3
	 * Description:使用枚举表述常量数据字段
	 */
	public enum SeckillStatEnum {
	    SUCCESS(1,"秒杀成功"),
	    END(0,"秒杀结束"),
	    REPEAT_KILL(-1,"重复秒杀"),
	    INNER_ERROR(-2,"系统异常"),
	    DATA_REWRITE(-3,"数据篡改");
	
	
	    private int state;
	    private String stateInfo;
	
	    SeckillStatEnum(int state, String stateInfo) {
	        this.state = state;
	        this.stateInfo = stateInfo;
	    }
	
	    public int getState() {
	        return state;
	    }
	
	    public String getStateInfo() {
	        return stateInfo;
	    }
	
	    public static SeckillStatEnum stateOf(int index){
	        for (SeckillStatEnum state:values()){
	            if (state.getState()==index){
	                return state;
	            }
	        }
	        return null;
	    }
	
	}
package com.fehead.service.Impl;
	
	import com.fehead.bean.Seckill;
	import com.fehead.bean.SuccessKilled;
	import com.fehead.dao.SeckillDao;
	import com.fehead.dao.SuccessKilledDao;
	import com.fehead.dao.cache.RedisDao;
	import com.fehead.dto.Exposer;
	import com.fehead.dto.SeckillExecution;
	import com.fehead.enums.SeckillStatEnum;
	import com.fehead.exception.RepeatKillException;
	import com.fehead.exception.SeckillCloseException;
	import com.fehead.exception.SeckillException;
	import com.fehead.service.SeckillService;
	import org.apache.commons.collections.MapUtils;
	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.HashMap;
	import java.util.List;
	import java.util.Map;
	
	/**
	 * Created by xiaoaxiao on 2019/5/3
	 * Description:
	 */
	@Service
	public class SeckillServiceImpl implements SeckillService {
	
	    private Logger logger = LoggerFactory.getLogger(this.getClass());
	    @Autowired
	    private SeckillDao seckillDao;
	    @Autowired
	    private SuccessKilledDao successKilledDao;
	    @Autowired
	    private RedisDao redisDao;
	
	    //md5盐值字符串,用于混淆md5
	    private final String slat = "uihwugwmo%$^&*(*#*(&";
	
	    public List<Seckill> getSeckillList() {
	        return seckillDao.queryAll(0, 4);
	    }
	
	    public Seckill getSeckillById(long seckillId) {
	        return seckillDao.queryById(seckillId);
	    }
	
	    public Exposer exportSeckillUrl(long seckillId) {
	
	        //优化点:缓存优化:超时维护一致性
	        //1、访问redis
	        Seckill seckill = redisDao.getSeckill(seckillId);
	        if (seckill == null) {
	            //2、访问数据库
	            seckill = seckillDao.queryById(seckillId);
	            //如果该秒杀根本不存在
	            if (seckill == null) {
	                return new Exposer(false, seckillId);
	            } else {
	                //3、放入到redis中
	                redisDao.putSeckill(seckill);
	            }
	        }
	//        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);
	    }
	
	    /**
	     * 通过seckillId获取一个独特的md5,md5的生成过程为 盐值字符串+base的规则,双重保障来完成md5的加密
	     *
	     * @param seckillId
	     * @return
	     */
	    private String getMD5(long seckillId) {
	        String base = seckillId + "/" + slat;
	        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
	        return md5;
	    }
	
	    @Transactional
	    /**
	     *使用注解控制事务方法的优点:
	     * 1、开发团队达成一致约定,明确标注事务方法的编程风格
	     * 2、保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
	     * 3、不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制
	     */
	    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, SeckillCloseException, RepeatKillException {
	
	        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 = successKilledDao.insertSuccessKilled(seckillId, userPhone);
	                //唯一:seckillId,userPhone
	                if (insertCount <= 0) {
	                    //重复秒杀,sql语句中的ignore就会返回0
	                    throw new RepeatKillException("seckill repeat");
	                } else {
	                    //秒杀成功
	                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
	                    return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
	                }
	            }
	        } catch (SeckillCloseException e1) {
	            throw e1;
	        } catch (RepeatKillException e2) {
	            throw e2;
	        } catch (Exception e) {
	            logger.error(e.getMessage(), e);
	            //把所有编译期异常,转化为运行期异常
	            throw new SeckillException("seckill inner error" + e.getMessage());
	        }
	    }
	
	    //通过redis+数据库中的存储过程来对秒杀过程进行优化
	    public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) throws SeckillException, SeckillCloseException, RepeatKillException {
	        if (md5 == null || !md5.equals(getMD5(seckillId))) {
	            return new SeckillExecution(seckillId, SeckillStatEnum.DATA_REWRITE);
	        }
	        Date killTime = new Date();
	        Map<String, Object> map = new HashMap<String, Object>();
	        map.put("seckillId", seckillId);
	        map.put("phone", userPhone);
	        map.put("killTime", killTime);
	        map.put("result", null);
	        //执行存储过程,result被复制
	        try {
	            seckillDao.killByProcedure(map);
	            //获取result
	            int result = MapUtils.getInteger(map, "result", -2);
	            if (result == 1) {
	                SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
	                return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, sk);
	            } else {
	                return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
	            }
	
	        } catch (Exception e) {
	            logger.error(e.getMessage(), e);
	            return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
	        }
	    }
	}

二、Spring装配Service实现类

<!-- 扫描service包及其子包下所有使用注解的类型-->
    <context:component-scan base-package="com.fehead.service"/>

三、Spring声明式事务配置

<beans xmlns="http://www.springframework.org/schema/beans"
	       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	       xmlns:p="http://www.springframework.org/schema/p"
	       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-3.0.xsd
	    http://www.springframework.org/schema/context
	    http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
	
	    <!-- 扫描service包及其子包下所有使用注解的类型-->
	    <context:component-scan base-package="com.fehead.service"/>
	
	    <!-- 配置事务管理器-->
	    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
	        <!--注入数据库连接池-->
	        <property name="dataSource" ref="dataSource"/>
	    </bean>
	
	    <!-- 配置基于注解的声明式事务
	        默认使用注解来管理事务行为
	    -->
	    <tx:annotation-driven transaction-manager="transactionManager"/>
	
	</beans>

四、Junit单元测试Service层

<?xml version="1.0" encoding="UTF-8" ?>
	<configuration>
	
	    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
	        <!-- encoders are assigned the type
	             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
	        <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>
package com.fehead.service;
	
	import com.fehead.bean.Seckill;
	import com.fehead.dto.Exposer;
	import com.fehead.dto.SeckillExecution;
	import com.fehead.exception.RepeatKillException;
	import com.fehead.exception.SeckillCloseException;
	import org.junit.Test;
	import org.junit.runner.RunWith;
	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.*;
	
	/**
	 * Created by xiaoaxiao on 2019/5/3
	 * Description:
	 */
	@RunWith(SpringJUnit4ClassRunner.class)
	@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> list = seckillService.getSeckillList();
	        logger.info("list={}", list);
	    }
	
	
	    @Test
	    public void getSeckillById() {
	        long id = 1000;
	        Seckill seckill = seckillService.getSeckillById(id);
	        logger.info("seckill={}",seckill);
	    }
	
	    @Test
	    /**
	     * Exposer{
	     * exposed=true,
	     * md5='fe95f6f9363d3286ddcb4f93e6082a4c',
	     * seckillId=1000,
	     * now=0, start=0, end=0}
	     */
	    public void exportSeckillUrl() {
	        long id = 1000;
	        //修改数据库把活动开启时间改为2019.5.1-2019.11.29
	        Exposer exposer = seckillService.exportSeckillUrl(id);
	        logger.info("exposer={}",exposer);
	    }
	
	    @Test
	    /**
	     * SeckillExecution{seckillId=1000, state=1, stateInfo='秒杀成功',
	     * successKilled=SuccessKilled{seckillId=1000, userPhone=12345678911,
	     * state=0, createTime=Fri May 03 22:18:31 CST 2019, seckill=Seckill{
	     * seckillId=1000, name='1000元秒杀iphone6', number=99, startTime=Fri May 03 22:18:31 CST 2019,
	     * endTime=Sat Nov 02 00:00:00 CST 2019, createTime=Mon Apr 29 16:46:23 CST 2019}}}
	     */
	    public void executeSeckill() {
	        long id = 1000;
	        long phone = 12345678911L;
	        String md5 = "fe95f6f9363d3286ddcb4f93e6082a4c";
	        //通过try-catch对已定义好了的异常进行相应的处理,就不会乱报错了
	        try {
	            SeckillExecution seckillExecution = seckillService.executeSeckill(id,phone,md5);
	            logger.info("result={}",seckillExecution);
	        }catch (RepeatKillException e){
	            logger.error(e.getMessage());
	        }catch (SeckillCloseException e){
	            logger.error(e.getMessage());
	        }
	    }
	
	    @Test
	    /**
	     * 集成测试代码完整逻辑,注意可重复执行——上面两个方法加起来,直接使用md5,而不是复制
	     */
	    public void testSeckillLogic() throws Exception{
	        long id = 1001;
	        Exposer exposer = seckillService.exportSeckillUrl(id);
	        if(exposer.isExposed()){
	            logger.info("exposer={}",exposer);
	            long phone = 12345678911L;
	            String md5 = "fe95f6f9363d3286ddcb4f93e6082a4c";
	            //通过try-catch对已定义好了的异常进行相应的处理,就不会乱报错了
	            try {
	                SeckillExecution seckillExecution = seckillService.executeSeckill(id,phone,md5);
	                logger.info("result={}",seckillExecution);
	            }catch (RepeatKillException e){
	                logger.error(e.getMessage());
	            }catch (SeckillCloseException e){
	                logger.error(e.getMessage());
	            }
	        }else {
	            //秒杀未开启
	            logger.warn("exposer={}",exposer);
	        }
	    }
	
	    @Test
	    public void executeSeckillProcedureTest(){
	        long seckillId = 1001;
	        long phone = 13687945684L;
	        Exposer exposer = seckillService.exportSeckillUrl(seckillId);
	        if(exposer.isExposed()){
	            String md5 = exposer.getMd5();
	            SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId,phone,md5);
	            logger.info(execution.getStateInfo());
	        }
	    }
	}