Java并发编程秒杀

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

2018-05-05  本文已影响38人  markfork

章节目录

创建基本的代码包层

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

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

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

代码如下:
业务逻辑接口声明类 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

相当于 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"/>

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

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

单元测试

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

上一篇下一篇

猜你喜欢

热点阅读