• 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html
  • 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html

整合SSM框架的高并发和商品秒杀项目(二)Java高并发秒杀API之Service层

技术杂谈 勤劳的小蚂蚁 1个月前 (02-08) 52次浏览 已收录 0个评论 扫描二维码


首先在编写Service层代码前,我们应该首先要知道这一层到底时干什么的,这里摘取来自ITEYE一位博主的原话
Service层主要负责业务模块的逻辑应用设计。同样是首先设计接口,再设计其实现的类,接着再Spring的配置文件中配置其实现的关联。这样我们就可以在应用中调用Service接口来进行业务处理。Service层的业务实现,具体要调用到已定义的DAO层的接口,封装Service层的业务逻辑有利于通用的业务逻辑的独立性和重复利用性,程序显得非常简洁。
在项目中要降低耦合的话,分层是一种很好的概念,就是各层各司其职,尽量不做不相干的事,所以Service层的话顾名思义就是业务逻辑,处理程序中的一些业务逻辑,以及调用DAO层的代码,这里我们的DAo层就是连接数据库的那一层,调用关系可以这样表达:
View(页面)>Controller(控制层)>Service(业务逻辑)>Dao(数据访问)>Database(数据库)
  • 首先还是接口的设计,设计Service秒杀商品的接口 SeckillService 首先在som.suny包下建立interfaces这个包,这个包里面存放Service相关的接口,然后建立SeckillService接口文件,代码如下:

publicinterfaceSeckillService{

 /**
  *  查询全部的秒杀记录.
  * @return 数据库中所有的秒杀记录
  */
 List<Seckill> getSeckillList();

 /**
  *   查询单个秒杀记录
  * @param seckillId   秒杀记录的ID
  * @return   根据ID查询出来的记录信息
  */
 Seckill getById(long seckillId);

 /**
  * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址
  * @param seckillId  秒杀商品Id
  * @return  根据对应的状态返回对应的状态实体
  */
 Exposer exportSeckillUrl(long seckillId);

 /**
  * 执行秒杀操作,有可能是失败的,失败我们就抛出异常
  * @param seckillId  秒杀的商品ID
  * @param userPhone 手机号码
  * @param md5   md5加密值
  * @return   根据不同的结果返回不同的实体信息
  */
 SeckillExecution executeSeckill(long seckillId,long userPhone,String md5)throws SeckillException,RepeatKillException,SeckillCloseException;

建立后接口之后我们要写实现类了,在写实现类的时候我们肯定会碰到一个这样的问题,你要向前端返回json数据的话,你是返回什么样的数据好?直接返回一个数字状态码或者时文字?这样设计肯定是不好的,所以我们应该向前段返回一个实体信息json,里面包含了一系列的信息,无论是哪种状态都应该可以应对,既然是与数据库字段无关的类,那就不是PO了,所以我们建立一个DTO数据传输类,关于常见的几种对象我的解释如下:
  • PO: 也就是我们在为每一张数据库表写一个实体的类
  • VO, 对某个页面或者展现层所需要的数据,封装成一个实体类
  • BO, 就是业务对象,我也不是很了解
  • DTO, 跟VO的概念有点混淆,也是相当于页面需要的数据封装成一个实体类
  • POJO, 简单的无规则java对象
com.suny下建立dto包,然后建立Exposer类,这个类是秒杀时数据库那边处理的结果的对象
publicclassExposer{
 /*是否开启秒杀 */
 privateboolean exposed;
 /*  对秒杀地址进行加密措施  */
 private String md5;
 /* id为seckillId的商品秒杀地址   */
 privatelong seckillId;
 /* 系统当前的时间   */
 private LocalDateTime now;
 /* 秒杀开启的时间   */
 private LocalDateTime start;
 /*  秒杀结束的时间  */
 private LocalDateTime end;

 publicExposer(){
 }

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

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

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

 publicbooleanisExposed(){
     return exposed;
 }

 publicvoidsetExposed(boolean exposed){
     this.exposed = exposed;
 }

 public String getMd5(){
     return md5;
 }

 publicvoidsetMd5(String md5){
     this.md5 = md5;
 }

 publiclonggetSeckillId(){
     return seckillId;
 }

 publicvoidsetSeckillId(long seckillId){
     this.seckillId = seckillId;
 }

 public LocalDateTime getNow(){
     return now;
 }

 publicvoidsetNow(LocalDateTime now){
     this.now = now;
 }

 public LocalDateTime getStart(){
     return start;
 }

 publicvoidsetStart(LocalDateTime start){
     this.start = start;
 }

 public LocalDateTime getEnd(){
     return end;
 }

 publicvoidsetEnd(LocalDateTime end){
     this.end = end;
 }

 @Override
 public String toString(){
     return“Exposer{“ +
             “秒杀状态=” + exposed +
             “, md5加密值='” + md5 + ”’ +
             “, 秒杀ID=” + seckillId +
             “, 当前时间=” + now +
             “, 开始时间=” + start +
             “, 结束=” + end +
             ‘}’;
 }
}

然后我们给页面返回的数据应该是更加友好的封装数据,所以我们再在com.suny.dto包下再建立SeckillExecution用来封装给页面的结果:

publicclassSeckillExecution{

   privatelong seckillId;
   /* 执行秒杀结果的状态   */
   privateint state;
   /* 状态的明文标示   */
   private String stateInfo;
   /*  当秒杀成功时,需要传递秒杀结果的对象回去  */
   private SuccessKilled successKilled;

   /*  秒杀成功返回的实体  */
   publicSeckillExecution(long seckillId, int state, String stateInfo, SuccessKilled successKilled){
       this.seckillId = seckillId;
       this.state = state;
       this.stateInfo = stateInfo;
       this.successKilled = successKilled;
   }

   /*  秒杀失败返回的实体  */
   publicSeckillExecution(long seckillId, int state, String stateInfo){
       this.seckillId = seckillId;
       this.state = state;
       this.stateInfo = stateInfo;
   }

   publiclonggetSeckillId(){
       return seckillId;
   }

   publicvoidsetSeckillId(long seckillId){
       this.seckillId = seckillId;
   }

   publicintgetState(){
       return state;
   }

   publicvoidsetState(int state){
       this.state = state;
   }

   public String getStateInfo(){
       return stateInfo;
   }

   publicvoidsetStateInfo(String stateInfo){
       this.stateInfo = stateInfo;
   }

   public SuccessKilled getSuccessKilled(){
       return successKilled;
   }

   publicvoidsetSuccessKilled(SuccessKilled successKilled){
       this.successKilled = successKilled;
   }

   @Override
   public String toString(){
       return“SeckillExecution{“ +
               “秒杀的商品ID=” + seckillId +
               “, 秒杀状态=” + state +
               “, 秒杀状态信息='” + stateInfo + ”’ +
               “, 秒杀的商品=” + successKilled +
               ‘}’;
   }
}
定义秒杀中可能会出现的异常
  • 定义一个基础的异常,所有的子异常继承这个异常SeckillException

/**
*  秒杀基础异常
* Created by 孙
*/
publicclassSeckillExceptionextendsRuntimeException{
 publicSeckillException(String message){
     super(message);
 }

 publicSeckillException(String message, Throwable cause){
     super(message, cause);
 }
}

+ 首选可能会出现秒杀关闭后被秒杀情况,所以建立秒杀关闭异常`SeckillCloseException`,需要继承我们一开始写的基础异常

/**
* 秒杀已经关闭异常,当秒杀结束就会出现这个异常
* Created by 孙
*/
publicclassSeckillCloseExceptionextendsSeckillException{
   publicSeckillCloseException(String message){
       super(message);
   }

   publicSeckillCloseException(String message, Throwable cause){
       super(message, cause);
   }
}

  • 然后还有可能发生重复秒杀异常RepeatKillException

/**
* 重复秒杀异常,不需要我们手动去try catch
* Created by 孙
*/
publicclassRepeatKillExceptionextendsSeckillException{
publicRepeatKillException(String message){
    super(message);
}

publicRepeatKillException(String message, Throwable cause){
    super(message, cause);
}
}

实现Service接口

@Service
publicclassSeckillServiceImplimplementsSeckillService{
   private Logger logger = LoggerFactory.getLogger(this.getClass());
   /* 加入一个盐值,用于混淆*/
   privatefinal String salt = “thisIsASaltValue”;

   @Autowired
   private SeckillMapper seckillMapper;
   @Autowired
   private SuccessKilledMapper successKilledMapper;


   /**
    * 查询全部的秒杀记录.
    *
    * @return 数据库中所有的秒杀记录
    */
   @Override
   public List<Seckill> getSeckillList(){
       return seckillMapper.queryAll(0, 4);
   }

   /**
    * 查询单个秒杀记录
    *
    * @param seckillId 秒杀记录的ID
    * @return 根据ID查询出来的记录信息
    */
   @Override
   public Seckill getById(long seckillId){
       return seckillMapper.queryById(seckillId);
   }

   /**
    * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址
    *
    * @param seckillId 秒杀商品Id
    * @return 根据对应的状态返回对应的状态实体
    */
   @Override
   public Exposer exportSeckillUrl(long seckillId){
       // 根据秒杀的ID去查询是否存在这个商品
      /* Seckill seckill = seckillMapper.queryById(seckillId);
       if (seckill == null) {
           logger.warn(“查询不到这个秒杀产品的记录”);
           return new Exposer(false, seckillId);
       }*/
       Seckill seckill = redisDao.getSeckill(seckillId);
       if (seckill == null) {
           // 访问数据库读取数据
           seckill = seckillMapper.queryById(seckillId);
           if (seckill == null) {
               returnnew Exposer(false, seckillId);
           } else {
               // 放入redis
               redisDao.putSeckill(seckill);
           }
       }

       // 判断是否还没到秒杀时间或者是过了秒杀时间
       LocalDateTime startTime = seckill.getStartTime();
       LocalDateTime endTime = seckill.getEndTime();
       LocalDateTime nowTime = LocalDateTime.now();
       //   开始时间大于现在的时候说明没有开始秒杀活动    秒杀活动结束时间小于现在的时间说明秒杀已经结束了
      /* if (!nowTime.isAfter(startTime)) {
           logger.info(“现在的时间不在开始时间后面,未开启秒杀”);
           return new Exposer(false, seckillId, nowTime, startTime, endTime);
       }
       if (!nowTime.isBefore(endTime)) {
           logger.info(“现在的时间不在结束的时间之前,可以进行秒杀”);
           return new Exposer(false, seckillId, nowTime, startTime, endTime);
       }*/
       if (nowTime.isAfter(startTime) && nowTime.isBefore(endTime)) {
           //秒杀开启,返回秒杀商品的id,用给接口加密的md5
           String md5 = getMd5(seckillId);
           returnnew Exposer(true, md5, seckillId);
       }
       returnnew Exposer(false, seckillId, nowTime, startTime, endTime);


   }

   private String getMd5(long seckillId){
       String base = seckillId + “/” + salt;
       return DigestUtils.md5DigestAsHex(base.getBytes());
   }

   /**
    * 执行秒杀操作,失败的,失败我们就抛出异常
    *
    * @param seckillId 秒杀的商品ID
    * @param userPhone 手机号码
    * @param md5       md5加密值
    * @return 根据不同的结果返回不同的实体信息
    */
   @Override
   public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)throws SeckillException {
       if (md5 == null || !md5.equals(getMd5(seckillId))) {
           logger.error(“秒杀数据被篡改”);
           thrownew SeckillException(“seckill data rewrite”);
       }
       // 执行秒杀业务逻辑
       LocalDateTime nowTIme = LocalDateTime.now();

       try {
           //执行减库存操作
           int reduceNumber = seckillMapper.reduceNumber(seckillId, nowTIme);
           if (reduceNumber <= 0) {
               logger.warn(“没有更新数据库记录,说明秒杀结束”);
               thrownew SeckillCloseException(“seckill is closed”);
           } else {
               // 这里至少减少的数量不为0了,秒杀成功了就增加一个秒杀成功详细
               int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone);
               // 查看是否被重复插入,即用户是否重复秒杀
               if (insertCount <= 0) {
                   thrownew RepeatKillException(“seckill repeated”);
               } else {
                   // 秒杀成功了,返回那条插入成功秒杀的信息
                   SuccessKilled successKilled = successKilledMapper.queryByIdWithSeckill(seckillId, userPhone);
//                    return new SeckillExecution(seckillId,1,”秒杀成功”);
                     returnnew SeckillExecution(seckillId,1,“秒杀成功”,successKilled);
               }
           }
       } catch (SeckillCloseException | RepeatKillException e1) {
           throw e1;
       } catch (Exception e) {
           logger.error(e.getMessage(), e);
           // 把编译期异常转换为运行时异常
           thrownew SeckillException(“seckill inner error : “ + e.getMessage());
       }
}

在这里我们捕获了运行时异常,这样做的原因就是Spring的事物默认就是发生了RuntimeException才会回滚,可以检测出来的异常是不会导致事物的回滚的,这样的目的就是你明知道这里会发生异常,所以你一定要进行处理.如果只是为了让编译通过的话,那捕获异常也没多意思,所以这里要注意事物的回滚.
然后我们还发现这里存在硬编码的现象,就是返回各种字符常量,例如秒杀成功,秒杀失败等等,这些字符串时可以被重复使用的,而且这样维护起来也不方便,要到处去类里面寻找这样的字符串,所有我们使用枚举类来管理这样状态,在con.suny包下建立enum包,专门放置枚举类,然后再建立SeckillStatEnum枚举类:

/**
* 常量枚举类
* Created by 孙
*/
publicenum SeckillStatEnum {
   SUCCESS(1, “秒杀成功”),
   END(0, “秒杀结束”),
   REPEAT_KILL(-1, “重复秒杀”),
   INNER_ERROR(-2, “系统异常”),
   DATE_REWRITE(-3, “数据篡改”);

   privateint state;
   private String info;

   SeckillStatEnum() {
   }

   SeckillStatEnum(int state, String info) {
       this.state = state;
       this.info = info;
   }

   publicintgetState() {
       return state;
   }

   public String getInfo() {
       return info;
   }

   publicstatic SeckillStatEnum stateOf(int index) {
       for (SeckillStatEnum statEnum : values()) {
           if (statEnum.getState() == index) {
               return statEnum;
           }
       }
       returnnull;
   }
}

既然把这些改成了枚举,那么在SeckillServiceImpl类中的executeSeckill方法中成功秒杀的返回值就应该修改为

returnnew SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);

改了这里以后会发现会报错,因为在实体类那边构造函数可不是这样的,然后修改SeckillExecution类的构造函数,把statestateInfo的值设置从构造函数里面的SeckillStatEnum中取出值来设置:

/*  秒杀成功返回的实体  */
   publicSeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled){
       this.seckillId = seckillId;
       this.state = statEnum.getState();
       this.stateInfo = statEnum.getInfo();
       this.successKilled = successKilled;
   }

   /*  秒杀失败返回的实体  */
   publicSeckillExecution(long seckillId, SeckillStatEnum statEnum){
       this.seckillId = seckillId;
       this.state = statEnum.getState();
       this.stateInfo = statEnum.getInfo();
   }

下一步肯定要注入Service了

首先在resources/spring下建立applicationContext-service.xml文件,用来配置Service层的相关代码:
<?xml version=“1.0” encoding=“UTF-8”?>
      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&#8221;>

   <!–配置自动扫描service包下的注解,在这里配置了自动扫描后,com.suny.service包下所有带有@Service注解的类都会被加入Spring容器中–>
   <context:component-scanbase-package=“com.suny.service”/>

   <!–配置事物,这里时使用基于注解的事物–>
   <beanid=“transactionManager”class=“org.springframework.jdbc.datasource.DataSourceTransactionManager”>
       <!–注入数据库连接池–>
       <propertyname=“dataSource”ref=“dataSource”/>
   </bean>

   <!–开启基于注解的申明式事物–>
   <tx:annotation-driventransaction-manager=“transactionManager”/>

</beans>

在这里开启了基于注解的事物,常见的事物操作有以下几种方法
  • 在Spring早期版本中是使用ProxyFactoryBean+XMl方式来配置事物.
  • 在Spring配置文件使用tx:advice+aop命名空间,好处就是一次配置永久生效,你无须去关心中间出的问题,不过出错了你很难找出来在哪里出了问题
  • 注解@Transactional的方式,注解可以在方法定义,接口定义,类定义,public方法上,但是不能注解在private,final,static等方法上,因为Spring的事物管理默认是使用Cglib动态代理的:
    • private方法因为访问权限限制,无法被子类覆盖
    • final方法无法被子类覆盖
    • static时类级别的方法,无法被子类覆盖
    • protected方法可以被子类覆盖,因此可以被动态字节码增强
不能被Spring AOP事物增强的方法
序号 动态代理策略 不能被事物增强的方法
1 基于接口的动态代理 出了public以外的所有方法,并且 public static 的方法也不能被增强
2 基于Cglib的动态代理 private,static,final的方法
然后你要在Service类上添加注解@Service,不用在接口上添加注解:
@Service
publicclassSeckillServiceImplimplementsSeckillService

既然已经开启了基于注解的事物,那我们就去需要被事物的方法上加个注解@Transactional吧:

@Transactional
   @Override
   public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException

Service层的测试

写测试类,我这里的测试类名为SeckillServiceImplTest:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({“classpath:spring/applicationContext-dao.xml”, “classpath:spring/applicationContext-service.xml”})
publicclassSeckillServiceImplTest{
   private Logger logger = LoggerFactory.getLogger(this.getClass());

   @Autowired
   private SeckillService seckillService;

   @Test
   publicvoidgetSeckillList()throws Exception {
       List<Seckill> seckillList = seckillService.getSeckillList();
       logger.info(seckillList.toString());
       System.out.println(seckillList.toString());
   }

   @Test
   publicvoidgetById()throws Exception {
       long seckillId = 1000;
       Seckill byId = seckillService.getById(seckillId);
       System.out.println(byId.toString());
   }

   @Test
   publicvoidexportSeckillUrl()throws Exception {
       long seckillId = 1000;
       Exposer exposer = seckillService.exportSeckillUrl(seckillId);
       System.out.println(exposer.toString());
   }

   @Test
   publicvoidexecuteSeckill()throws Exception {
       long seckillId = 1000;
       Exposer exposer = seckillService.exportSeckillUrl(seckillId);
       if (exposer.isExposed()) {
           long userPhone = 12222222222L;
           String md5 = “bf204e2683e7452aa7db1a50b5713bae”;
           try {
               SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5);
               System.out.println(seckillExecution.toString());
           } catch (SeckillCloseException | RepeatKillException e) {
               e.printStackTrace();
           }
       } else {
           System.out.println(“秒杀未开启”);
       }
   }

   @Test
   publicvoidexecuteSeckillProcedureTest(){
       long seckillId = 1001;
       long phone = 1368011101;
       Exposer exposer = seckillService.exportSeckillUrl(seckillId);
       if (exposer.isExposed()) {
           String md5 = exposer.getMd5();
           SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
           System.out.println(execution.getStateInfo());
       }
   }


}

测试的话如果每个方法测试都通过就说明通过,如果报错了话就仔细看下哪一步错了检查下

丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:整合SSM框架的高并发和商品秒杀项目(二)Java高并发秒杀API之Service层
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

您必须 登录 才能发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00