高并发秒杀业务 Service层

Service层的设计

创建需要的包

  1. service包:存放service的接口和实现类

  2. exception包:存放service所存在的一些异常(重复秒杀,秒杀关闭等)

  3. dto包:也是存放数据,和entity的区别在于,entity是业务的封装,dto是web和service之间的数据传递

接口SeckillService

站在使用者的角度去设计接口,而不是实现;使用者使用越方便越好
1. 方法定义粒度 2. 参数 3. 返回类型

  1. 查询所有的秒杀记录(为的是要有一个列表页,展示所有秒杀)
List<Seckill> getSeckillList();
  1. 查询单个秒杀记录
/**
     * 查询单个秒杀记录
     * @param seckillId
     * @return
     */
    Seckill getById(long seckillId);
  1. 输出秒杀接口的地址
    ① 秒杀开始的时候输出秒杀的接口地址
    ② 否则输出的是系统的时间和秒杀的时间
    这样的话秒杀没有开始的时候,谁也不知道我们的秒杀地址,用户没法提前猜到我们秒杀的地址,而提前使用浏览器插件等待秒杀
//秒杀开始输出秒杀接口地址
    //否则输出系统的时间和秒杀时间
    Exposer exportSeckillUrl(long seckillId);

这里的返回值Exposer是我们自己封装的dto对象,和业务无关,只是为了数据的传输更方便

package org.seckill.dto;

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


  1. 执行秒杀
    这一接口应该是在获取了秒杀的接口地址后进行,使用md5加密和内部的规则作比较,防止用户的url被篡改
/**
     * 执行秒杀
     * @param seckillId
     * @param userPhone
     * @param md5:md5加密和内部的规则作比较,防止用户的url被篡改
     */
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException, RepeatKillException, SeckillCloseException ;

返回值SeckillExecution也是我们自己封装的dto对象,如果秒杀成功返回秒杀Id,成功标识和成功对象,失败则不返回秒杀对象

package org.seckill.dto;

import org.seckill.entity.SuccessKilled;

/**
 * 封装秒杀结果
 */
public class SeckillExecution {
    private long seckillId;
    //秒杀执行结果状态
    private int state;
    //状态的标识
    private String stateInfo;
    //秒杀成功对象
    private SuccessKilled successKilled;

    public SeckillExecution(long seckillId, int state, String stateInfo, SuccessKilled successKilled) {
        this.seckillId = seckillId;
        this.state = state;
        this.stateInfo = stateInfo;
        this.successKilled = successKilled;
    }

    public SeckillExecution(long seckillId, int state, String stateInfo) {
        this.seckillId = seckillId;
        this.state = state;
        this.stateInfo = stateInfo;
    }

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

在执行秒杀的时候会有两个异常,重复秒杀异常和秒杀关闭异常
这都是运行时异常,即便不进行try catch,也不会有编译错误,而spring管理事务也都只能帮助我们回滚运行是异常
在我们设计的时候,应该有一个所有的秒杀相关业务的异常作为父类,其他异常由他细分
① 所有的秒杀相关业务的异常SeckillException

package org.seckill.exception;

/**
 * 所有的秒杀相关业务的异常
 */
public class SeckillException extends RuntimeException{
    public SeckillException(String message) {
        super(message);
    }

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

② 重复秒杀异常RepeatKillException

package org.seckill.exception;

/**
 * 重复秒杀异常(运行期异常)
 */
public class RepeatKillException extends SeckillException{
    public RepeatKillException(String message) {
        super(message);
    }

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

③ 秒杀关闭异常SeckillCloseException

package org.seckill.exception;

public class SeckillCloseException extends SeckillException {
    public SeckillCloseException(String message) {
        super(message);
    }

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

完整代码

package org.seckill.service;

import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;

import java.util.List;

public interface SeckillService {

    List<Seckill> getSeckillList();

    /**
     * 查询单个秒杀记录
     * @param seckillId
     * @return
     */
    Seckill getById(long seckillId);

    //秒杀开始输出秒杀接口地址
    //否则输出系统的时间和秒杀时间
    Exposer exportSeckillUrl(long seckillId);

    /**
     * 执行秒杀
     * @param seckillId
     * @param userPhone
     * @param md5:md5加密和内部的规则作比较,防止用户的url被篡改
     */
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException, RepeatKillException, SeckillCloseException ;


}

SeckillService的实现SeckillServiceImpl

实现放在service下的impl包下

  1. 返回秒杀对象的方法很简单,之间调用dao层的方法即可
@Override
    public List<Seckill> getSeckillList() {
        //只有4个秒杀对象
        return seckillDao.queryAll(0,4);
    }

    @Override
    public Seckill getById(long seckillId) {
        return seckillDao.queryById(seckillId);
    }
  1. exportSeckillUrl接口的实现
@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);
    }

其中把生成MD5单独抽出一个方法,为了在executeSeckill方法的重用
声明一个md5盐值用于混淆md5,因为如果只对skillId加密,很容易被猜出来,使用盐值和skillId的特定拼接再生成MD5更安全

private final String slat="sdaqei012ee[qsdaq231w";
   private String getMD5(long seckillId){
        //md5盐值用于混淆md5,因为如果只对skillId加密,很容易被才出来
        //使用盐值和skillId的特定拼接更安全
        String base = seckillId+"/"+slat;
        //使用spring的工具类生成md5
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }
  1. executeSeckill接口的实现
    这个就是对秒杀逻辑的执行
    ① 首先要去判断md5的正确性
if(md5 == null || !md5.equals(getMD5(seckillId))){
	throw new SeckillException("seckill data rewrite");
}

② 接下来就是执行秒杀逻辑:减库存+记录购买记录
先去更新秒杀,没有更新到记录(商品没有了或者不在秒杀时间)则秒杀结束,抛出异常

int updateCount = seckillDao.reduceNumber(seckillId,nowTime);
if(updateCount <= 0){//没有更新到记录则秒杀结束
	throw new SeckillCloseException("seckill is closed");
}
           

③ 更新秒杀成功后要去记录购买记录

int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
                //userPhone和seckillId作为联合主键避免重复秒杀
if(insertCount<=0){
	throw new RepeatKillException("seckill repeated");
}else {//秒杀成功
	SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
	return new SeckillExecution(seckillId,1,"秒杀成功",successKilled);
}

④ 对于减库存和记录购买记录应该组成一个事务,要么都成功,要么都失败

 @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 = successKilledDao.insertSuccessKilled(seckillId,userPhone);
                //userPhone和seckillId作为联合主键避免重复秒杀
                if(insertCount<=0){
                    throw new RepeatKillException("seckill repeated");
                }else {//秒杀成功
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId,1,"秒杀成功",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());
        }
    }

⑤ 逻辑实现完成,但是并不够友好,也就是说,new SeckillExecution(seckillId,1,“秒杀成功”,successKilled);里面的1和“秒杀成功”应该都是常量,我们用枚举来记录,更易于组织
枚举类如下

package org.seckill.enums;

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

    private int state;
    private String stateInf;

    SeckillStaEnum(int state, String stateInf) {
        this.state = state;
        this.stateInf = stateInf;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }

    public String getStateInf() {
        return stateInf;
    }

    public void setStateInf(String stateInf) {
        this.stateInf = stateInf;
    }

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

更改SeckillExecution的构造方法

 public SeckillExecution(long seckillId, SeckillStaEnum seckillStaEnum, SuccessKilled successKilled) {
        this.seckillId = seckillId;
        this.state = seckillStaEnum.getState();
        this.stateInfo = seckillStaEnum.getStateInf();
        this.successKilled = successKilled;
    }

    public SeckillExecution(long seckillId, SeckillStaEnum seckillStaEnum) {
        this.seckillId = seckillId;
        this.state = seckillStaEnum.getState();
        this.stateInfo = seckillStaEnum.getStateInf();
    }

更改后完整代码如下

@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 = successKilledDao.insertSuccessKilled(seckillId,userPhone);
                //userPhone和seckillId作为联合主键避免重复秒杀
                if(insertCount<=0){
                    throw new RepeatKillException("seckill repeated");
                }else {//秒杀成功
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStaEnum.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());
        }
    }

使用Spring托管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"
       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">
    <!--扫描service包下所有使用注解的类型-->
    <context:component-scan base-package="org.seckill.service"></context:component-scan>
</beans>

Spring声明式事务

在这里插入图片描述
我们不用再去关心什么时候开始事务,什么时候结束事务,什么时候回滚,而都交给spring管理
在这里插入图片描述
事务方法的嵌套
在这里插入图片描述
当新的事务加进来的时候,如果之前的有事务则直接加入原有的事务逻辑,没有则创建新的事务

什么时候回滚
当抛出运行期异常的时候就会回滚

配置

<?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"></context:component-scan>

    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource">
        </property>
    </bean>

    <!--配置基于注解的声明事务-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

最终的service实现类

package org.seckill.service.impl;

import org.seckill.dao.SeckillDao;
import org.seckill.dao.SuccessKilledDao;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.entity.SuccessKilled;
import org.seckill.enums.SeckillStaEnum;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
import org.seckill.service.SeckillService;
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 javax.xml.crypto.Data;
import java.util.Date;
import java.util.List;

@Service
public class SeckillServiceImpl implements SeckillService {
    //日志对象
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    //实现类在容器当中,注入dao层
    @Autowired
    private SeckillDao seckillDao;
    @Autowired
    private SuccessKilledDao successKilledDao;

    //md5盐值用于混淆md5,因为如果只对skillId加密,很容易被才出来
    //使用盐值和skillId的特定拼接更安全
    private final String slat="sdaqei012ee[qsdaq231w";

    @Override
    public List<Seckill> getSeckillList() {
        //只有4个秒杀对象
        return seckillDao.queryAll(0,4);
    }

    @Override
    public Seckill getById(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);
    }

    @Override
    @Transactional
    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 = successKilledDao.insertSuccessKilled(seckillId,userPhone);
                //userPhone和seckillId作为联合主键避免重复秒杀
                if(insertCount<=0){
                    throw new RepeatKillException("seckill repeated");
                }else {//秒杀成功
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStaEnum.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());
        }
    }

    private String getMD5(long seckillId){
        //md5盐值用于混淆md5,因为如果只对skillId加密,很容易被才出来
        //使用盐值和skillId的特定拼接更安全
        String base = seckillId+"/"+slat;
        //使用spring的工具类生成md5
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }
}

发布了62 篇原创文章 · 获赞 0 · 访问量 1169

猜你喜欢

转载自blog.csdn.net/weixin_43907800/article/details/103987217
今日推荐