1. 程式人生 > >Java高併發秒殺API之service層實現(二)

Java高併發秒殺API之service層實現(二)

二 service層實現

1.內容

站在使用者的角度設計介面
三個方向 :方法粒度,引數,返回型別

2.程式碼

SeckillService

package org.seckill.service;

import java.util.List;

import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.exception.RepeatKillException;
import
org.seckill.exception.SeckillException; import org.seckill.exception.seckillCloseException; /** * 站在使用者角度設計介面 * 三個方面 方法粒度 引數 返回型別 */ public interface SeckillService { /** * * 查詢所有秒殺記錄 * @return */ List <Seckill> getSeckillList(); /** * * 查詢單個秒殺記錄 * @param seckillId * @return
*/
Seckill getById(long seckillId); /** * 輸出秒殺介面地址 否則輸出系統時間和秒殺時間 * @param seckillId */ Exposer exportSeckillUrl(long seckillId); //執行秒殺操作 SeckillExecution excuteSeckill(long seckillId,long userPhone,String md5) throws RepeatKillException,seckillCloseException,SeckillException; //執行秒殺操作by儲存過程
SeckillExecution excuteSeckillProcedure(long seckillId,long userPhone,String md5) throws RepeatKillException,seckillCloseException,SeckillException; }

重複秒殺異常 RepeatKillException

package org.seckill.exception;
//重複秒殺異常(執行期異常)
public class RepeatKillException extends RuntimeException{

    public RepeatKillException(String message, Throwable cause) {
        super(message, cause);
        // TODO Auto-generated constructor stub
    }

    public RepeatKillException(String message) {
        super(message);
        // TODO Auto-generated constructor stub
    }


}

秒殺關閉異常 seckillCloseException

package org.seckill.exception;
/**
 * 
 * 秒殺關閉異常
 * @author LCJA
 *
 */
public class seckillCloseException extends RuntimeException{

    public seckillCloseException(String message, Throwable cause) {
        super(message, cause);
        // TODO Auto-generated constructor stub
    }

    public seckillCloseException(String message) {
        super(message);
        // TODO Auto-generated constructor stub
    }

}

秒殺相關的所有業務異常 SeckillException

package org.seckill.exception;
/**
 * 
 * 秒殺相關的所有業務異常
 * @author LCJA
 *
 */
public class SeckillException extends RuntimeException{

    public SeckillException(String message, Throwable cause) {
        super(message, cause);
        // TODO Auto-generated constructor stub
    }

    public SeckillException(String message) {
        super(message);
        // TODO Auto-generated constructor stub
    }


}

SeckillServiceImpl 實現類

package org.seckill.service.impl;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.MapUtils;
import org.seckill.dao.SeckillDao;
import org.seckill.dao.SuccessKilledDao;
import org.seckill.dao.cache.RedisDao;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.entity.SuccessKilled;
import org.seckill.enums.SeckillStatEnum;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillException;
import org.seckill.exception.seckillCloseException;
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;
//@component  @service @dao @conroller
@Service
public class SeckillServiceImpl implements SeckillService{
    private Logger logger=LoggerFactory.getLogger(this.getClass());
    // mabaits 使用mapper形式 注入spring容器中
    //注入service依賴
    @Autowired//@[email protected]
    private SeckillDao seckilldao;
    @Autowired
    private SuccessKilledDao successkilleddao;
    @Autowired
    private RedisDao redisdao;

    //用於混淆md5
    private final String slat="abcd";
    public List<Seckill> getSeckillList() {
        // TODO Auto-generated method stub
        return seckilldao.queryAll(0, 4);
    }

    public Seckill getById(long seckillId) {
        // TODO Auto-generated method stub
        return seckilldao.queryById(seckillId);
    }

    public Exposer exportSeckillUrl(long seckillId) {
        //優化點 : 快取優化
        /**
         * get from cache
         * if null
         * get db
         * else 
         *  put cache 
         *  locgin
         *  可以這樣做的原因 秒殺單 一般不做修改
         *  超時維護
         *  一般做叢集  
         */
        //訪問redis
        Seckill seckill = redisdao.getSeckill(seckillId);
        if(seckill == null){
             seckill=seckilldao.queryById(seckillId);
             System.out.println("seckill----"+seckill);
                if(seckill== null){
                    return new Exposer(false, seckillId);
                }else{
                    //放入redis
                    redisdao.putSeckill(seckill);
                }

        }

        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);
    }
/**
 * 使用註解控制事務有點
 * 開發團隊達成一致約定 明確事務方法的程式設計風格
 * 保證事務方法的執行時間竟可能短不要穿插其他網路操作
 * 不是所有的方法都需要事務 如只有一條修改操作 ,只讀操作不需要控制事務
 * 通過異常來告訴宣告式事務 回滾還是commit
 */@Transactional
    public SeckillExecution excuteSeckill(long seckillId, long userPhone, String md5)
            throws RepeatKillException, seckillCloseException, SeckillException {
        try{
        if(md5==null||!md5.equals(getMD5(seckillId))){
            throw new SeckillException("seckill data rewrite");
        }
        int insertCount=successkilleddao.insertSuccessKilled(seckillId, userPhone);
        if(insertCount<=0){
            //重複秒殺
            throw new RepeatKillException("seckill repeated");
        }else{
            //執行秒殺邏輯 減庫存 +記錄購買行為
            //減庫存熱點商品競爭
            int updateCount = seckilldao.reduceNumber(seckillId, new Date());
            if(updateCount <= 0){
                //沒有更新到記錄,秒殺結束
                throw new seckillCloseException("seckill losed");
            }else{
                //減庫存成功 記錄購買行為
                //秒殺成功
                SuccessKilled successKilled=successkilleddao.queryByIdWithSeckill(seckillId, userPhone);
                return new SeckillExecution(seckillId,SeckillStatEnum.SUCCESS,successKilled);       
            }
        }
        }catch(Exception e){
            logger.error(e.getMessage(),e);
            throw new SeckillException("seckill inner error:"+e.getMessage());
        }
    }
    private String getMD5(long seckillId){
        String base=seckillId+"/"+slat;
        String md5=DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }

    public SeckillExecution excuteSeckillProcedure(long seckillId, long userPhone, String md5)
            throws RepeatKillException, seckillCloseException, SeckillException {
        if(md5==null||!md5.equals(getMD5(seckillId))){
            return  new SeckillExecution(seckillId,SeckillStatEnum.DATA_REWRITE);
        }
        Date killTime = new Date();
        Map <String, Object>map =new HashMap<String, Object>();
        map.put("seckillId", seckillId);
        map.put("phone", userPhone);
        map.put("killTime", killTime);
        map.put("result", null);
    try {
        seckilldao.killByProcedure(map);
        int result = MapUtils.getInteger(map, "result",-2);
        if(result==1){
            SuccessKilled sk = successkilleddao.queryByIdWithSeckill(seckillId, userPhone);
            return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS,sk);
        }else{
            return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
        }
    } catch (Exception e) {
        logger.error(e.getMessage());
        return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
    }
    }

}

SeckillStatEnum

package org.seckill.enums;
//使用列舉表示常量資料欄位
public enum SeckillStatEnum {
    SUCCESS(1,"秒殺成功"),
    END(0,"秒殺結束"),
    REPEAT_KILL(-1,"重複秒殺"),
    INNER_ERROR(-2,"系統異常"),
    DATA_REWRITE(-3,"資料篡改")
    ;
    private int state;

    private String stateInfo;

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

    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 static SeckillStatEnum stateOf(int index){
        for (SeckillStatEnum state :values()){
            if(state.getState() == index){
                return state;
            }
        }
        return null;
    }

}

3.spring託管service

spring會通過spring工廠建立物件

seckillservice 依賴 SeckillDao和SuccessKillDao,
SeckillDao和SuccessKillDao依賴SqlSessionFactory,
SqlSessionFactory 依賴 資料來源..

原因
物件建立統一管理,
規範生命週期管理,
靈活的依賴注入,
一致獲取物件。

ioc使用場景

xml 註解 配置類
1.bean實現類來自第三方類庫,如DataSource等;2需要名稱空間配置,如:context,aop,mvc等 專案中自身開發使用的類,可直接在程式碼中使用註解 如@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:aop="http://www.springframework.org/schema/aop" 
        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>

4.spring宣告式事務

丟擲執行期異常時,會回滾,小心try-catch

這個是 tx:advice+aop名稱空間  一次配置永久生效(看起來很美好),本篇沒有采用
<!-- 配置事務事務屬性 -->
     <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
           <tx:method name="add*" propagation="REQUIRED" />
            <tx:method name="append*" propagation="REQUIRED" />
            <tx:method name="insert*" propagation="REQUIRED" />
            <tx:method name="save*" propagation="REQUIRED" />
            <tx:method name="update*" propagation="REQUIRED" />
            <tx:method name="modify*" propagation="REQUIRED" />
            <tx:method name="edit*" propagation="REQUIRED" />
            <tx:method name="delete*" propagation="REQUIRED" />
            <tx:method name="remove*" propagation="REQUIRED" />
            <tx:method name="get*" propagation="SUPPORTS" />
            <tx:method name="*" propagation="SUPPORTS" />
        </tx:attributes>
    </tx:advice>
    <!-- 配置事務切點,並把切點和事務屬性關聯起來 -->
    <aop:config>
        <aop:pointcut expression="execution(* com.jinlou.service..*Impl.*(..))" id="txPointcut"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>
本篇採用的是註解的形式
  <!-- 配置註解式宣告事務 -->
        <tx:annotation-driven transaction-manager="transactionManager"/>

使用註解控制事務的優點;
1.開發團隊達成一致約定,明確標註事務方法的程式設計風格
2.保證事務執行時儘可能短,不要穿插其他網路操作(rpc(遠端服務呼叫),http請求)
3.不是所有的方法都不是需要事務

5.單元測試

package org.seckill.service;

import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.service.SeckillService;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({
    "classpath:spring/spring-dao.xml",
    "classpath:spring/spring-service.xml"
        })
public class SeckillServiceTest {
    private Logger logger=LoggerFactory.getLogger(this.getClass());
    @Autowired
private SeckillService seckillService;
    @Test
    public void testGetSeckillList() {
        List <Seckill> list=seckillService.getSeckillList();
        logger.info("list={}",list);
    }

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

    @Test
    public void testExportSeckillUrl() {
        long id=1000;
        Exposer exposer=  seckillService.exportSeckillUrl(id);
        logger.info("exposer={}",exposer);
    }
    //exposed=true, md5=8e7add56132475b9141111ed8e961613, seckillId=1000, now=0, start=0, end=0]

    @Test
    public void testExcuteSeckill() {
        long id=1000;
        long phone=12345679;
        String md5="8e7add56132475b9141111ed8e961613";
        SeckillExecution seckillexcution =seckillService.excuteSeckill(id, phone, md5);
        logger.info("seckillexcution={}",seckillexcution);
    }
    @Test
    public  void executeSeckillProcedure(){
        long seckillId =1000;
        long phone =1313131313;
        Exposer exposer = seckillService.exportSeckillUrl(seckillId);
        if(exposer.isExposed()){
            String md5 = exposer.getMd5();
            SeckillExecution excution = seckillService.excuteSeckillProcedure(seckillId, phone, md5);
            logger.info(excution.getStateInfo());
        }


    }

}

logback.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 預設往控制檯列印 -->
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
    <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>

另外可以列印sql

<bean id="druid-stat-interceptor"
        class="com.alibaba.druid.support.spring.stat.DruidStatInterceptor">
    </bean>
    <bean id="druid-stat-pointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"
        scope="prototype">
        <property name="patterns">
            <list>
                <value>com.skwx.service.impl.*</value>
            </list>
        </property>
    </bean>
    <aop:config>
        <aop:advisor advice-ref="druid-stat-interceptor" pointcut-ref="druid-stat-pointcut" />
    </aop:config>

需要在pom中新增依賴

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.27</version>
    </dependency>