从零开发高并发秒杀系统(二)Service层

DAO层工作:接口设计+SQL编写。对各种数据存储系统进行存取,不应该有逻辑程序

Service层工作:DAO拼接等逻辑。

entity:业务的封装

dto:service层和web层数据的传递,一般与具体业务无关

业务接口的设计:站在使用者角度设计接口,主要有三个方面

(1)方法定义粒度:粒度应该明确,而不是去关注具体实现

(2参数:参数应该简练明确,不冗余

(3)返回类型(return和异常):返回类型应该友好

一、service业务接口设计

dto类

package com.jiangwangxiang.dto;

//暴露秒杀接口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;
  }

  @Override
  public String toString() {
    return "Exposer [exposed=" + exposed + ", md5=" + md5 + ", seckillId=" + seckillId + ", now=" + now + ", start="
      + start + ", end=" + end + "]";
  }
}

package com.jiangwangxiang.dto;

import com.jiangwangxiang.entity.SuccessKilled;
import com.jiangwangxiang.enums.SeckillStateEnum;

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

  public SeckillExecution(long seckillId, SeckillStateEnum stateEnum, SuccessKilled successKilled) {
    this.seckillId = seckillId;
    this.state = stateEnum.getState();
    this.stateInfo = stateEnum.getStateInfo();
    this.successKilled = successKilled;
  }

  public SeckillExecution(long seckillId, SeckillStateEnum stateEnum) {
    this.seckillId = seckillId;
    this.state = stateEnum.getState();
    this.stateInfo = stateEnum.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.jiangwangxiang.enums;

//使用枚举表述常量数据字典
public enum SeckillStateEnum {
  SUCCESS(1, "秒杀成功"), END(0, "秒杀结束"), REPEAT_KILL(-1, "重发秒杀"), INNER_ERROR(-2, "系统异常"), DATA_REWRITE(-3, "数据篡改");
  private int state;
  private String stateInfo;

  private SeckillStateEnum(int state, String stateInfo) {
    this.state = state;
    this.stateInfo = stateInfo;
  }

  public int getState() {
    return state;
  }

  public String getStateInfo() {
    return stateInfo;
  }

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

秒杀相关业务异常

java异常有两种:编译期异常和运行时异常。运行时异常不需要显式try-catch,更重要的是spring声明式事务只支持运行时异常回滚策略,也就是如果在spring的声明式事务

中抛出了运行时异常,spring的声明式事务就会回滚,而如果是非运行时异常则不会回滚。

package com.jiangwangxiang.exception;

//秒杀相关业务异常
public class SeckillException extends RuntimeException {
  private static final long serialVersionUID = 1L;

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

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

package com.jiangwangxiang.exception;

//重复秒杀异常(运行期异常)
public class RepeatKillException extends SeckillException {
  private static final long serialVersionUID = 1L;

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

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

package com.jiangwangxiang.exception;

//秒杀关闭异常
public class SeckillCloseException extends SeckillException {
  private static final long serialVersionUID = 1L;

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

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

service业务接口

package com.jiangwangxiang.service;

import java.util.List;

import com.jiangwangxiang.dto.Exposer;
import com.jiangwangxiang.dto.SeckillExecution;
import com.jiangwangxiang.entity.Seckill;
import com.jiangwangxiang.exception.RepeatKillException;
import com.jiangwangxiang.exception.SeckillCloseException;
import com.jiangwangxiang.exception.SeckillException;

//业务接口:站在"使用者"角度设计接口 三个方面:方法定义粒度,参数,返回类型(return 类型/异常)
public interface SeckillService {
  //查询所有秒杀记录
  List<Seckill> getSeckillList();

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

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

  //执行秒杀操作
  SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
    throws SeckillException, RepeatKillException, SeckillCloseException;
}

service业务接口实现类

package com.jiangwangxiang.service.impl;

import java.util.Date;
import java.util.List;

import com.jiangwangxiang.dao.SeckillDao;
import com.jiangwangxiang.dao.SuccessKilledDao;
import com.jiangwangxiang.dto.Exposer;
import com.jiangwangxiang.dto.SeckillExecution;
import com.jiangwangxiang.entity.Seckill;
import com.jiangwangxiang.exception.RepeatKillException;
import com.jiangwangxiang.exception.SeckillCloseException;
import com.jiangwangxiang.exception.SeckillException;
import com.jiangwangxiang.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;

@Service
public class SeckillServiceImpl implements SeckillService {

  private Logger logger = LoggerFactory.getLogger(this.getClass());

  // 注入Service依赖
  @Autowired
  private SeckillDao seckillDao;

  @Autowired
  private SuccessKilledDao successKilledDao;

  // md5盐值字符串,用于混淆MD5
  private final String slat = "skdfjksjdf7787%^%^%^FSKJFK*(&&%^%&^8DF8^%^^*7hFJDHFJ";

  @Override
  public List<Seckill> getSeckillList() {
    return seckillDao.queryAll(0, 4);
  }

  @Override
  public Seckill getById(long seckillId) {
    return seckillDao.queryById(seckillId);
  }

  private String getMD5(long seckillId) {
    String base = seckillId + "/" + slat;
    String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
    return md5;
  }

  @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
  /**
  * 使用注解控制事务方法的优点: 1.开发团队达成一致约定,明确标注事务方法的编程风格
  * 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作,RPC/HTTP请求或者剥离到事务方法外部
  * 3.不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制
  */
  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 now = new Date();
    try {
      // 记录购买行为
      int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
      // 唯一:seckillId,userPhone
      if (insertCount <= 0) {
        // 重复秒杀
        throw new RepeatKillException("seckill repeated");
      } else {
        // 减库存,热点商品竞争
        int updateCount = seckillDao.reduceNumber(seckillId, now);
        if (updateCount <= 0) {
          // 没有更新到记录 rollback
          throw new SeckillCloseException("seckill is closed");
        } else {
          // 秒杀成功 commit
          SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
          return new SeckillExecution(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("seckill inner error:" + e.getMessage());
    }
  }
}

二、基于spring托管service实现类及配置并使用spring声明式事务

什么是声明式事务?

事务包含:开启事务+多个SQL语句+提交/回滚事务 ,声明式事务的目的就是解脱事务代码,也就是我们在编写事务代码时不需要编写开启事务和提交/回滚事务的代码,只需要

在有@Transactional注解的方法中编写事务涉及到的SQL语句即可实现事务的功能。

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="com.jiangwangxiang.service" />

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

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

事务方法的嵌套是spring声明式事务独有的概念,主要体现在传播行为上,即当有一个新的事务加入进来的时候,如果原有事务存在,则直接加入到原有事务。

那spring声明式事务什么时候回滚?当事务方法抛出运行期异常(RuntimeException)时spring会回滚事务

日志配置   logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <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>

三、集成测试Service逻辑

package com.jiangwangxiang.seckill.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
* 配置spring和junit整合,junit启动时加载springIOC容器 spring-test,junit
*/
@RunWith(SpringJUnit4ClassRunner.class)
// 告诉junit spring配置文件
@ContextConfiguration({ "classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml" })
public class SeckillServiceImplTest {
  private final Logger logger = LoggerFactory.getLogger(this.getClass());

  @Autowired
  private SeckillService seckillService;

  @Test
  public void testGetSeckillList() throws Exception {
    List<Seckill> list = seckillService.getSeckillList();
    logger.info("list={}", list);
  }

  @Test
  public void testGetById() throws Exception {
    long id = 1000;
    Seckill seckill = seckillService.getById(id);
    logger.info("seckill={}", seckill);
  }

  // 测试代码完整逻辑,注意可重复执行
  @Test
  public void testSeckillLogic() throws Exception {
    long id = 1001;
    Exposer exposer = seckillService.exportSeckillUrl(id);
    if (exposer.isExposed()) {
      logger.info("exposer={}", exposer);
      long phone = 13631231234L;
      String md5 = exposer.getMd5();
      try {
        SeckillExecution execution = seckillService.executeSeckill(id, phone, md5);
        logger.info("execution={}", execution);
      } catch (RepeatKillException e) {
        logger.error(e.getMessage());
      } catch (SeckillCloseException e) {
        logger.error(e.getMessage());
      }
    } else {
      // 秒杀未开启
      logger.error("exposer={}", exposer);
    }
  }
}

 

posted @ 2018-03-06 22:31  将王相  阅读(80)  评论(0)    收藏  举报