Java高并发秒杀业务Api-Service层构建过程

章节目录

  • 秒杀Service 接口开发工作
    • 秒杀业务逻辑编写
    • spring-IOC 管理 service 组件
      • context:component-scan
    • Spring 声明式事务
    • junit测试

创建基本的代码包层

1.创建DTO - 数据传输层对象

网络数据到达Controller 层后会使用框架自带的数据绑定 以及反序列化为dto对
象,并作为参数传递至service层进行处理。

2.业务接口实现
注意:业务接口的实现需要站在使用者的角度去设计接口

  • 方法定义粒度-非常明确,参数简练,直接,不要一个大map对象去传递,return 还可以抛出异常。

代码如下:
业务逻辑接口声明类 SecKillService.java

package org.seckill.service;

import org.seckill.domain.SecKill;
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;

/**
 * 业务接口的实现需要站在使用者的角度去设计接口
 * 三个方面:方法定义粒度、参数、返回类型 dto就可以
 */
public interface SecKillService {


    /**
     * 返回秒杀商品列表
     *
     * @return
     */
    List<SecKill> getSecKillList();


    /**
     * 查询秒杀商品单条记录
     *
     * @param secKillId
     * @return
     */
    SecKill getSecKillById(long secKillId);


    /**
     * 秒杀开启时,输出秒杀接口的地址,否则输出系统时间,和秒杀时间
     *
     * @param secKillId
     * @return
     */
    Exposer exportSecKillUrl(long secKillId);


    /**
     * 执行秒杀操作
     * 验证当前的excuteSecKill id 与 传递过来的md5是否相同
     *
     * @param secKillId
     * @param userPhone
     * @param md5
     */
    SecKillExcution excuteSecKill(long secKillId, String userPhone, String md5)
            throws SecKillException, RepeatKillException, SecKillCloseException;

}

业务逻辑接口实现类-SecKillServiceImpl

package org.seckill.service.impl;

import org.seckill.dao.SecKillDao;
import org.seckill.dao.SuccessKilledDao;
import org.seckill.domain.SecKill;
import org.seckill.domain.SuccessKilled;
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.SecKillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.DigestUtils;

import java.util.Date;
import java.util.List;
@Service
public class SecKillServiceImpl implements SecKillService {
    //在业务逻辑层打日志
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowire
    private SecKillDao secKillDao;
    @Autowire
    private SuccessKilledDao successKilledDao;

    //md5加盐,混淆加密
    private final String salt = "asdasd8Zy*&ZCY87ywer7t678tzt67wer";

    public List<SecKill> getSecKillList() {
        return secKillDao.queryAll(0, 4);
    }

    public SecKill getSecKillById(long secKillId) {
        return secKillDao.queryById(secKillId);
    }

    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 = null;
        return new Exposer(secKillId, md5, true);
    }

    /**
     * 生成对应秒杀商品的md5值,做参数校验
     * 保证可重用
     *
     * @param secKillId
     * @return
     */
    private String getMD5(long secKillId) {
        String base = secKillId + "/" + salt;//用户不知道salt
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }

    /**
     * 执行秒杀
     *
     * @param secKillId
     * @param userPhone
     * @param md5
     * @return
     * @throws SecKillException
     * @throws RepeatKillException
     * @throws SecKillCloseException
     */
    public SecKillExcution excuteSecKill(long secKillId, String userPhone, String md5) throws SecKillException, RepeatKillException, SecKillCloseException {
        try {
            //1.用户输入的md5值验证
            if (md5 == null || !md5.equals(getMD5(secKillId))) {
                throw new SecKillException("seckill data rewrite");
            }

            //2.减库存、执行秒杀逻辑+记录购买行为,执行秒杀时间
            Date nowTime = new Date();
            int updateCount = secKillDao.reduceStock(secKillId, nowTime);
            if (updateCount <= 0) {
                //没有更新到记录,秒杀结束
                throw new SecKillCloseException("seckill is closed");
            } else {
                //3.记录购买行为 insertSuccessKilled 可能出现数据库连接超时等问题,所以需要外层try
                int insertCount = successKilledDao.insertSuccessKilled(secKillId, userPhone);
                //唯一验证,secKillId + userPhone
                if (insertCount <= 0) {//数据库联合主键冲突
                    throw new RepeatKillException("seckill repeated");
                } else {//秒杀成功,返回秒杀成功的实体
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSecKill(secKillId, userPhone);
                    //不优雅的实现方式,在多处需要用到提示信息时,我们可以采用统一的常量去返回,这样待客户端提示语改变时,我们可以统一进行更改。
//                    return new SecKillExcution(secKillId, 1, "秒杀成功", 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());
            //所有异常最终会转化为 运行时异常,spring 的声明式事务会帮我们做rollback。
            throw new SecKillException("seckill inner error" + e.getMessage());
        }
    }
}

数据传输层类- Exposer

package org.seckill.dto;

/**
 * 暴露秒杀接口
 */
public class Exposer {
    private boolean exposed;//秒杀是否开启标志位
    private String md5;     //加密措施,暴露地址包括一个md5值
    private long now;       //系统当前时间(毫秒),方便浏览器计算距离服务器秒杀开启时间
    private long secKillId; //秒杀商品的id
    private long start;
    private long end;

    //秒杀正在进行
    public Exposer(long secKillId, String md5, boolean exposed) {
        this.secKillId = secKillId;
        this.md5 = md5;
        this.exposed = exposed;
    }
    //秒杀结束或还没开启
    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 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;
    }

    public long getSecKillId() {
        return secKillId;
    }

    public void setSecKillId(long secKillId) {
        this.secKillId = secKillId;
    }

    @Override
    public String toString() {
        return "Exposer{" +
                "exposed=" + exposed +
                ", md5='" + md5 + '\'' +
                ", now=" + now +
                ", secKillId=" + secKillId +
                ", start=" + start +
                ", end=" + end +
                '}';
    }
}

数据传输层-SecKillExcution

package org.seckill.dto;

import org.seckill.domain.SuccessKilled;
import org.seckill.enums.SecKillStateEnum;

/**
 * 执行秒杀之后的结果
 */
public class SecKillExcution {
    private long secKillId;

    private int state;//状态的标识

    private String stateInfo;//状态表示

    private SuccessKilled successSecKilled;//秒杀成功对象

    //成功 jakson 在转化枚举的时候会出现问题,不支持枚举序列化
    public SecKillExcution(long secKillId, SecKillStateEnum secKillStateEnum, SuccessKilled successSecKilled) {
        this.secKillId = secKillId;
        this.state = secKillStateEnum.getState();
        this.stateInfo = secKillStateEnum.getStateInfo();
        this.successSecKilled = successSecKilled;
    }

    //失败,使用到枚举
    public SecKillExcution(long secKillId, SecKillStateEnum secKillStateEnum) {
        this.secKillId = secKillId;
        this.state = secKillStateEnum.getState();
        this.stateInfo = secKillStateEnum.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 getSuccessSecKilled() {
        return successSecKilled;
    }

    public void setSuccessSecKilled(SuccessKilled successSecKilled) {
        this.successSecKilled = successSecKilled;
    }

    @Override
    public String toString() {
        return "SecKillExcution{" +
                "secKillId=" + secKillId +
                ", state=" + state +
                ", stateInfo='" + stateInfo + '\'' +
                ", successSecKilled=" + successSecKilled +
                '}';
    }
}

业务运行时异常类-SecKillException、RepeatKillException、SecKillCloseException

package org.seckill.exception;

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

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

package org.seckill.exception;

/**
 * 重复秒杀的异常(运行时异常)
 * 声明式事务,只接收运行时异常
 */
public class RepeatKillException extends SecKillException {

    public RepeatKillException(String message) {
        super(message);
    }

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

package org.seckill.exception;

/**
 * 秒杀关闭异常-友好响应给用户
 */
public class SecKillCloseException extends SecKillException {
    public SecKillCloseException(String message) {
        super(message);
    }

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

2.spring-IOC管理 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" />

    <!-- 配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据库连接池-->
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- 配置基于注解的声明式事务
         默认使用注解来管理事务行为
    -->
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

Spring 声明式事务

什么是声明式事务

声明式事务

底层原理:采用动态代理的方式为我们的事务核心逻辑添加开启事务、回滚提交事务的控制操作

声明式事务使用方式

声明式事务

推荐使用@Transactional

声明式事务的传播行为
定义:即事务方法的嵌套
一个业务需要调用多个声明了事务控制的方法,那么最新组合的事务是重新启动一个事务,还是说沿用老的事务呢?

默认的事务传播行为:
propagation_required
浅析如下:
ServiceA {  
         
     void methodA() {  
         ServiceB.methodB();  
     }  
    
}  
    
ServiceB {  
         
     void methodB() {  
     }  
         
}  

比如说ServiceB.methodB 事务传播行为定义为PROPAGATION_REQUIRED

  • 那么当ServiceA.methodA 调用 ServiceB.methodB 时,methodA起了新的事
    务,那么ServiceB.methodB看到自己已经运行在ServiceA.methodA的事务内
    部,就不再起新的事务。
  • 而假如ServiceA.methodA运行的时候发现自己没有在事务中,他就会为自己
    分配一个事务。这样,在ServiceA.methodA或者在ServiceB.methodB内的任何
    地方出现异常,事务都会被回滚。即使ServiceB.methodB的事务已经被提交,
    但是ServiceA.methodA在接下来fail要回滚,ServiceB.methodB也要回滚

相当于 methodB 通过自己的事务传播行为告诉methodA 自己使用事务的原
则,告诉methodA 你要有事务我methodB就用你的,如果methodA没有事务,
那你methodA就需要创建一个事务。

什么时候回滚事务
当业务方法抛出运行时异常(RuntimeException)的时候spring 事务管理器会进行commit

配置事务管理器

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource">
</bean>

配置基于注解的声明式事务

<tx:annotation-driven transaction-manager="transactionManager"/>

使用注解控制事务方法的优点

  • 开发团队达成一致约定,约定明确标注事务方法的编程风格
  • 保证事务方法的执行时间尽可能的短,不要穿插其他的网络操作,RPC/HTTP请求,如果必须需要的话,那么将这些请求剥离出来,形成一个干净的方法调用。不要混合编写和外部系统进行网络通信的代码。
  • 不是所有的方法都需要事务,select、insert操作,单条语句的insert、update都不需要事务操作、不需要并发控制&多个操作联合形成一个事务时,不需要设置事务,因为mysql有autocommit=1的设置。

上述是性能杀手啊 ,注意再注意。

单元测试

package org.seckill.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.domain.SecKill;
import org.seckill.dto.Exposer;
import org.seckill.dto.SecKillExcution;
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;

@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() throws Exception {
        List<SecKill> secKillList = secKillService.getSecKillList();
        logger.info("list{}", secKillList);
    }

    @Test
    public void getSecKillById() throws Exception {
        long secKillId = 1000L;
        SecKill secKill = secKillService.getSecKillById(secKillId);
        logger.info("seckill{}", secKill);
    }

    @Test
    public void exportSecKillUrl() throws Exception {
        long secKillId = 1000L;
        Exposer exposer = secKillService.exportSecKillUrl(secKillId);
        logger.info("exposer{}", exposer);
    }

    @Test
    public void excuteSecKill() throws Exception {
        long secKillId = 1000L;
        String userPhone = "15300815981";
        String md5 = "6e3cc65f3b42e656bdbc55a6a381f5d0";
        SecKillExcution secKillExcution = secKillService.excuteSecKill(secKillId, userPhone, md5);
        logger.info("secKillExcution{}", secKillExcution);
    }

}

intellj idea 下单元测试快捷键:ctrl+shift+t

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,647评论 18 139
  • 一、事务的基本原理Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事...
    阿灯_supwinr阅读 14,759评论 2 28
  • 新闻热词 1.广场舞:square dancing 2.不得扰乱公共秩序: not disturb public ...
    Eva汪阅读 247评论 0 0
  • 风搅春花香已尽,天长重了头。夜静残月心难修,孤影两鬓秋。 神往异域风情好,轻启泛心舟。恨是清河变浊流,怎承载,万千愁。
    玮山阅读 182评论 0 0
  • 我是一名典型的拖延症患者。 学生时代,不到考试前的最后一刻绝不复习。毕业后,老板交代的任务不到deadline的那...
    蓝四阅读 490评论 2 5