1. 程式人生 > >SpringBoot2.0.3+Mybatis+Mysql+druid實現讀寫分離+事務

SpringBoot2.0.3+Mybatis+Mysql+druid實現讀寫分離+事務

       mysql支援一主多從,即在寫庫的資料庫發生變動時,會同步到所有從庫,只是同步過程中,會有一定的延遲(除非業務中出現,立即寫立即讀,否則稍微的延遲是可以接收的)。

       當資料庫有主從之分了,那應用程式碼也應該讀寫分離了。這時候的事務就不像單個數據庫那麼簡單了,為此整理出解決事務問題的方案:使用AbstractRoutingDataSource+aop+annotation在service層決定資料來源,可以支援事務。此方案的缺點:類內部方法通過this.xx()方式相互呼叫時,aop不會進行攔截,需進行特殊處理。

       下面對這一方案做詳細介紹。

        1、POM.xml

 <!-- 定義公共資源版本 -->
	<parent>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-parent</artifactId>
	    <version>2.0.3.RELEASE</version>
	    <relativePath /> 
	</parent>
	<properties>
	    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	    <java.version>1.8</java.version>
	</properties>
	
	<dependencies>
	    <!-- 上邊引入 parent,因此 下邊無需指定版本 -->
	    <!-- 包含 mvc,aop 等jar資源 -->
	    <dependency>
	        <groupId>org.springframework.boot</groupId>
	        <artifactId>spring-boot-starter-web</artifactId>
	        <exclusions>
	            <exclusion><!-- 去除預設log配置 -->
	                <groupId>org.springframework.boot</groupId>
	                <artifactId>spring-boot-starter-logging</artifactId>
	            </exclusion>
	        </exclusions>
	    </dependency>
	    
	    <!-- 配置log4j2 -->
	    <dependency>
	        <groupId>org.springframework.boot</groupId>
	        <artifactId>spring-boot-starter-log4j2</artifactId>
	    </dependency>
	    <!-- 配置log4j2 -->
	    
	    <!-- 支援識別yml配置 -->
	    <dependency>
	        <groupId>com.fasterxml.jackson.dataformat</groupId>
	        <artifactId>jackson-dataformat-yaml</artifactId>
	    </dependency>
	    <!-- 支援識別yml配置 -->
	    
	    <!-- 熱部署 -->
		<dependency>
		    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-devtools</artifactId>
		    <optional>true</optional>
		    <scope>true</scope>
		</dependency>
		<!-- 熱部署 -->
        
        <!-- AOP的配置 -->
        <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- AOP的配置 -->
        
        <!-- springboot,mybatis 整合包 -->
		<dependency>
		    <groupId>org.mybatis.spring.boot</groupId>
		    <artifactId>mybatis-spring-boot-starter</artifactId>
		    <version>1.3.2</version>
		</dependency>
		<!-- springboot,mybatis 整合包 -->
		
		<!-- mysql 驅動包 -->
		<dependency>
		    <groupId>mysql</groupId>
		    <artifactId>mysql-connector-java</artifactId>
		</dependency>
		<!-- mysql 驅動包 -->
		
		<!-- springboot測試 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- springboot測試 -->
		
		<dependency>
		    <groupId>junit</groupId>
		    <artifactId>junit</artifactId>
		    <scope>test</scope>
		</dependency>
		
		<!-- ali的druid -->
		<dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>druid-spring-boot-starter</artifactId>
           <version>1.1.10</version>
        </dependency>
        <!-- ali的druid -->

		
        <!--開始 JSONObject物件依賴的jar包 -->
        <dependency>
		    <groupId>commons-beanutils</groupId>
		    <artifactId>commons-beanutils</artifactId>
		    <version>1.9.3</version>
		</dependency>
		<dependency>
		    <groupId>commons-collections</groupId>
		    <artifactId>commons-collections</artifactId>
		    <version>3.2.2</version>
		</dependency>
		<dependency>
		    <groupId>commons-lang</groupId>
		    <artifactId>commons-lang</artifactId>
		    <version>2.6</version>
		</dependency>
		<dependency>
		    <groupId>commons-logging</groupId>
		    <artifactId>commons-logging</artifactId>
		    <version>1.2</version>
		</dependency>
		<dependency>
		    <groupId>net.sf.ezmorph</groupId>
		    <artifactId>ezmorph</artifactId>
		    <version>1.0.6</version>
		</dependency>
		<dependency>
		    <groupId>net.sf.json-lib</groupId>
		    <artifactId>json-lib</artifactId>
		    <version>2.4</version>
		    <classifier>jdk15</classifier>
		</dependency>
        <!--結束  JSONObject物件依賴的jar包 -->
        
    </dependencies>

	<build>
	    <plugins>
		    <plugin>
	            <groupId>org.springframework.boot</groupId>
	            <artifactId>spring-boot-maven-plugin</artifactId>
	            <configuration>
	                <!-- 沒有該配置,devtools 不生效 -->
	                <fork>true</fork>
	            </configuration>
	        </plugin>

	    </plugins>
	</build>

2、 application.yml和application-dev.yml

        本例子的資料庫,都是在本地的mysql中建立2個庫,test,test_01,例子是為了測試程式碼的讀寫分離。下面是application.yml:

spring: 
  profiles:
    active: dev
  thymeleaf:
    cache: true
    prefix: classpath:/templates/
    suffix: .html
    mode: HTML5
    encoding: UTF-8
    servlet:
      content-type: text/html

mybatis:
  config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml 

 下面是application-dev.yml:

server:
  port: 8080

spring:
  aop:
      proxy-target-class: true
  datasource:
    #readSize為從庫數量
    readSize: 1
    ###################以下為druid增加的配置###########################
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password: root
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        min-evictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: true
        testOnReturn: false
        poolPreparedStatements: true
        useGlobalDataSourceStat: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,wall
        WebStatFilter:
          exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
          enabled: true
          urlPattern: '/*'
        StatViewServlet:
          enabled: true
          urlPattern: '/druid/*'
          loginUsername: druid
          loginPassword: druid
      slave:
        url: jdbc:mysql://127.0.0.1:3306/test_01?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password: root
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        min-evictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: true
        testOnReturn: false
        poolPreparedStatements: true
        useGlobalDataSourceStat: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,wall
        WebStatFilter:
          exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
          enabled: true
          urlPattern: '/*'
        StatViewServlet:
          enabled: true
          urlPattern: '/druid/*'
          loginUsername: druid
          loginPassword: druid
    ###############以上為配置druid新增的配置########################################

3、mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 獲取資料庫自增主鍵值 -->
        <setting name="useGeneratedKeys" value="true"/>
        <!-- 使用列別名替換列名,預設為 true -->
        <setting name="useColumnLabel" value="true"/>
        <!-- 開啟駝峰命名轉換:Table(create_time) => Entity(createTime) -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
</configuration>

4、DataSourceConfiguration.java(讀取配置多個數據源) 

package com.wocloud.arch.ssm.mybatis;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class DataSourceConfiguration {
    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;

    @Bean(name = "writeDataSource", destroyMethod = "close", initMethod = "init")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.druid.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    @Bean(name = "readDataSource", destroyMethod = "close", initMethod = "init")
    @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

   @Bean("readDataSources")
    public List<DataSource> readDataSources() throws SQLException {
        List<DataSource> dataSources = new ArrayList<>();
        //dataSources.add(masterDataSource());
        dataSources.add(slaveDataSource());
        return dataSources;
    }
}

5、MybatisConfiguration.java(資料庫的sqlSessionFactorys、roundRobinDataSouceProxy、sqlSessionTemplate、annotationDrivenTransactionManager的設定。重點是roundRobinDataSouceProxy()方法,它把所有的資料庫源交給MyAbstractRoutingDataSource類,這個類見第6項,並由它的determineCurrentLookupKey()進行決定資料來源的選擇,其中讀庫進行了簡單的以輪詢的方式的負載均衡) 

package com.wocloud.arch.ssm.mybatis;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.wocloud.arch.ssm.utils.SpringContextUtil;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


@Configuration
@AutoConfigureAfter(DataSourceConfiguration.class)
@ConditionalOnClass({EnableTransactionManagement.class})
@Import({DataSourceConfiguration.class})
@MapperScan(basePackages = {"com.wocloud.arch.ssm.mapper"})
public class MybatisConfiguration {
    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;
    @Value("${spring.datasource.readSize}")
    private String dataSourceSize;

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(ApplicationContext ac) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(roundRobinDataSouceProxy(ac));
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mybatis/mapper/*.xml"));
        sqlSessionFactoryBean.setTypeAliasesPackage("com.wocloud.arch.ssm.model");
        sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * 有多少個數據源就要配置多少個bean
     */
    @Bean
    public AbstractRoutingDataSource roundRobinDataSouceProxy(ApplicationContext ac) {
        int size = Integer.parseInt(dataSourceSize);
        MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(size);
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();

        DataSource writeDataSource = (DataSource) ac.getBean("writeDataSource");
        List<DataSource> readDataSources = (List<DataSource>) ac.getBean("readDataSources");
        for (int i = 0; i < size; i++) {
            targetDataSources.put(i, readDataSources.get(i));
        }
        proxy.setDefaultTargetDataSource(writeDataSource);
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }
    
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    	return new SqlSessionTemplate(sqlSessionFactory);
    }
    
    //事務管理
    @Bean
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return new DataSourceTransactionManager((DataSource)SpringContextUtil.getBean("roundRobinDataSouceProxy"));
    }
    
}

6、MyAbstractRoutingDataSource.java

package com.wocloud.arch.ssm.mybatis;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.concurrent.atomic.AtomicInteger;

public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
    private final int dataSourceNumber;
    private AtomicInteger count = new AtomicInteger(0);

    public MyAbstractRoutingDataSource(int dataSourceNumber) {
        this.dataSourceNumber = dataSourceNumber;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DataSourceContextHolder.getJdbcType();
        if (DataSourceType.write.getType().equals(typeKey) || typeKey == null) {
            return DataSourceType.write.getType();
        }
        // 讀 簡單負載(因為從庫數量為1,實際上目前沒有負載效果)
        int number = count.getAndAdd(1);
        int lookupKey = number % dataSourceNumber;
        return new Integer(lookupKey);
    }
}

7、DataSourceType.java()

package com.wocloud.arch.ssm.mybatis;

public enum DataSourceType {
    read("read", "從庫"),
    write("write", "主庫");
    private String type;
    private String name;

    DataSourceType(String type, String name) {
        this.type = type;
        this.name = name;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

8、DataSourceContextHolder.java

package com.wocloud.arch.ssm.mybatis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DataSourceContextHolder {
    private static final ThreadLocal<String> local = new ThreadLocal<String>();

    public static ThreadLocal<String> getLocal() {
        return local;
    }

    private final static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);

    public static void read() {
        local.set(DataSourceType.read.getType());
        logger.info("切換到讀庫...");
    }

    public static void write() {
        local.set(DataSourceType.write.getType());
        logger.info("切換到寫庫...");
    }

    public static String getJdbcType() {
        return local.get();
    }
}

 9、寫庫、讀庫的註解

package com.wocloud.arch.ssm.mybatis;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadDataSource {
    String description() default "";
}
package com.wocloud.arch.ssm.mybatis;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteDataSource {
    String description() default "";
}

10、 DataSourceAop.java(事務的決定者)

package com.wocloud.arch.ssm.mybatis;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;

/*
 * 在service層決定資料來源
 * 
 * 必須在事務AOP之前執行,所以實現Ordered,order的值越小,越先執行
 * 如果一旦開始切換到寫庫,則之後的讀都會走寫庫
 */

@Aspect
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
@Component
public class DataSourceAop implements PriorityOrdered {
    private final static Logger logger = LoggerFactory.getLogger(DataSourceAop.class);

    @Pointcut("@annotation(com.wocloud.arch.ssm.mybatis.WriteDataSource)")
    public void writeMethod(){}

    @Pointcut("@annotation(com.wocloud.arch.ssm.mybatis.ReadDataSource)")
    public void readMethod(){}

    @Before("writeMethod() and execution(* com.wocloud.arch.ssm.service.impl..*.*(..)) ")
    public void beforeWrite(JoinPoint point) {
        //設定資料庫為寫資料
        DataSourceContextHolder.write();
        //除錯程式碼,可註釋
        String className = point.getTarget().getClass().getName();
        String methodName = point.getSignature().getName();
        logger.info("dataSource切換到:Write 開始執行:" + className + "." + methodName + "()方法...");
    }

    @Before("readMethod() and execution(* com.wocloud.arch.ssm.service.impl..*.*(..)) ")
    public void beforeRead(JoinPoint point) throws ClassNotFoundException {
        //設定資料庫為讀資料
        DataSourceContextHolder.read();
        //除錯程式碼,可註釋
        String className = point.getTarget().getClass().getName();//根據切點獲取當前呼叫的類名
        String methodName = point.getSignature().getName();//根據切點獲取當前呼叫的類方法
        logger.info("dataSource切換到:Read 開始執行:" + className + "." + methodName + "()方法...");
//        Object[] args = point.getArgs();//根據切點獲取當前類方法的引數
//        Class reflexClassName = Class.forName(className);//根據反射獲取當前呼叫類的例項
//        Method[] methods = reflexClassName.getMethods();//獲取該例項的所有方法
//        for (Method method : methods) {
//            if (method.getName().equals(methodName)) {
//                String desrciption = method.getAnnotation(ReadDataSource.class).description();//獲取該例項方法上註解裡面的描述資訊
//                System.out.println("desrciption:" + desrciption);
//            }
//        }
    }

	
	@Override
	public int getOrder() {
		/**
		 * 值越小,越優先執行
		 * 要優於事務的執行
		 * 在啟動類中加上了@EnableTransactionManagement(order = 10) 
		 */
		return 1;
	}
}

11、CdkeyMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wocloud.arch.ssm.mapper.CdkeyMapper">
  <resultMap id="BaseResultMap" type="com.wocloud.arch.ssm.model.Cdkey">
    <!--
      WARNING - @mbggenerated
      This element is automatically generated by MyBatis Generator, do not modify.
    -->
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="cdkey" jdbcType="VARCHAR" property="cdkey" />
    <result column="order_code" jdbcType="VARCHAR" property="orderCode" />
    <result column="isusage" jdbcType="VARCHAR" property="isusage" />
    <result column="issend" jdbcType="SMALLINT" property="issend" />
    <result column="first_time" jdbcType="TIMESTAMP" property="firstTime" />
    <result column="last_time" jdbcType="TIMESTAMP" property="lastTime" />
    <result column="order_code_wo" jdbcType="VARCHAR" property="orderCodeWo" />
    <result column="flag1" jdbcType="VARCHAR" property="flag1" />
    <result column="flag2" jdbcType="VARCHAR" property="flag2" />
    <result column="flag3" jdbcType="INTEGER" property="flag3" />
    <result column="flag4" jdbcType="INTEGER" property="flag4" />
  </resultMap>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
    <!--
      WARNING - @mbggenerated
      This element is automatically generated by MyBatis Generator, do not modify.
    -->
    delete from cdkey
    where id = #{id,jdbcType=BIGINT}
  </delete>
  <insert id="insert" parameterType="com.wocloud.arch.ssm.model.Cdkey">
    <!--
      WARNING - @mbggenerated
      This element is automatically generated by MyBatis Generator, do not modify.
    -->
    <selectKey keyProperty="id" order="BEFORE" resultType="java.lang.Long">
      SELECT LAST_INSERT_ID()
    </selectKey>
    insert into cdkey (id, cdkey, order_code, 
      isusage, issend, first_time, 
      last_time, order_code_wo, flag1, 
      flag2, flag3, flag4
      )
    values (#{id,jdbcType=BIGINT}, #{cdkey,jdbcType=VARCHAR}, #{orderCode,jdbcType=VARCHAR}, 
      #{isusage,jdbcType=VARCHAR}, #{issend,jdbcType=SMALLINT}, #{firstTime,jdbcType=TIMESTAMP}, 
      #{lastTime,jdbcType=TIMESTAMP}, #{orderCodeWo,jdbcType=VARCHAR}, #{flag1,jdbcType=VARCHAR}, 
      #{flag2,jdbcType=VARCHAR}, #{flag3,jdbcType=INTEGER}, #{flag4,jdbcType=INTEGER}
      )
  </insert>
  <update id="updateByPrimaryKey" parameterType="com.wocloud.arch.ssm.model.Cdkey">
    <!--
      WARNING - @mbggenerated
      This element is automatically generated by MyBatis Generator, do not modify.
    -->
    update cdkey
    set cdkey = #{cdkey,jdbcType=VARCHAR},
      order_code = #{orderCode,jdbcType=VARCHAR},
      isusage = #{isusage,jdbcType=VARCHAR},
      issend = #{issend,jdbcType=SMALLINT},
      first_time = #{firstTime,jdbcType=TIMESTAMP},
      last_time = #{lastTime,jdbcType=TIMESTAMP},
      order_code_wo = #{orderCodeWo,jdbcType=VARCHAR},
      flag1 = #{flag1,jdbcType=VARCHAR},
      flag2 = #{flag2,jdbcType=VARCHAR},
      flag3 = #{flag3,jdbcType=INTEGER},
      flag4 = #{flag4,jdbcType=INTEGER}
    where id = #{id,jdbcType=BIGINT}
  </update>
  <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    <!--
      WARNING - @mbggenerated
      This element is automatically generated by MyBatis Generator, do not modify.
    -->
    select id, cdkey, order_code, isusage, issend, first_time, last_time, order_code_wo, 
    flag1, flag2, flag3, flag4
    from cdkey
    where id = #{id,jdbcType=BIGINT}
  </select>
  <select id="selectAll" resultMap="BaseResultMap">
    <!--
      WARNING - @mbggenerated
      This element is automatically generated by MyBatis Generator, do not modify.
    -->
    select id, cdkey, order_code, isusage, issend, first_time, last_time, order_code_wo, 
    flag1, flag2, flag3, flag4
    from cdkey
  </select>
  <select id="selectCdkeyByIsUsage" parameterType="java.lang.String" resultMap="BaseResultMap">
    select id, cdkey, order_code, isusage, issend, first_time, last_time, order_code_wo,
    flag1, flag2, flag3, flag4
    from cdkey where 1=1
    <if test="_parameter!=null">
    and isusage = #{isusage,jdbcType=VARCHAR} limit 1
    </if>
    FOR UPDATE
  </select>
  <select id="selectCdkeyByOrderCode" parameterType="java.lang.String" resultMap="BaseResultMap">
    select id, cdkey, order_code, isusage, issend, first_time, last_time, order_code_wo, 
    flag1, flag2, flag3, flag4
    from cdkey where 1=1
    <if test="_parameter!=null">
    and order_code = #{orderCode,jdbcType=VARCHAR}
    </if>
  </select>
  
</mapper>

 12、CdkeyMapper.java

package com.wocloud.arch.ssm.mapper;

import com.wocloud.arch.ssm.model.Cdkey;
import java.util.List;

import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CdkeyMapper {
    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table cdkey
     *
     * @mbggenerated
     */
    int deleteByPrimaryKey(Long id);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table cdkey
     *
     * @mbggenerated
     */
    int insert(Cdkey record);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table cdkey
     *
     * @mbggenerated
     */
    Cdkey selectByPrimaryKey(Long id);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table cdkey
     *
     * @mbggenerated
     */
    List<Cdkey> selectAll();

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table cdkey
     *
     * @mbggenerated
     */
    int updateByPrimaryKey(Cdkey record);
    /*
     * 查詢可用兌換碼
     */
    Cdkey selectCdkeyByIsUsage(String isUsage);
    
    /*
     * 由訂單號查詢兌換碼是否已傳送
     */
    Cdkey selectCdkeyByOrderCode(String orderCode);
}

13、CdkeyService.java

/**
 * 
 */
package com.wocloud.arch.ssm.service;

import java.util.List;

import com.wocloud.arch.ssm.model.Cdkey;

/**
 * @author mazhen
 *
 */
public interface CdkeyService {
	/*
     * 插入Cdkey資訊
     * @param Cdkey
     * @return 
     */
	public int insertCdkey(Cdkey cdkey);
	
	/*
     * 查詢Cdkey資訊
     * @param cdkeyId
     * @return  Cdkey
     */
	public Cdkey queryByCdkeyId(Long cdkeyId);
	
	/*
     * 刪除Cdkey資訊
     * @param cdkeyId
     * @return  
     */
	public int deleteByCdkeyId(Long cdkeyId);
	
	/*
     * 更新Cdkey資訊
     * @param Cdkey
     * @return  
     */
	public int updateCdkey(Cdkey cdkey);
	
	/*
     * 查詢所有Cdkey資訊
     * @param 
     * @return  
     */
	public List<Cdkey> queryAll();
	
	/*
     * 根據簡訊狀態查詢可用Cdkey兌換碼
     * @param messageStatus
     * @return  Cdkey
     */
	public Cdkey queryCdkeyByIsUsage(String isUsage);
	
	/*
     * 根據訂單號查詢Cdkey兌換碼是否已存在
     * @param orderCode
     * @return  Cdkey
     */
	public Cdkey queryCdkeyByOrderCode(String orderCode);
    
    /*
	 * 兌換碼是否使用,cdkey表中更新資料
	 */
	public String updateCdkeyIsusage(JSONObject jsonObject);
}

14、CdkeyServiceImpl.java(使用getService()的原因:

/**
 * 
 */
package com.wocloud.arch.ssm.service.impl;

import java.util.List;

import com.wocloud.arch.ssm.mybatis.ReadDataSource;
import com.wocloud.arch.ssm.mybatis.WriteDataSource;

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 com.wocloud.arch.ssm.mapper.CdkeyMapper;
import com.wocloud.arch.ssm.model.Cdkey;
import com.wocloud.arch.ssm.service.CdkeyService;
import com.wocloud.arch.ssm.utils.SpringContextUtil;


import net.sf.json.JSONObject;


/**
 * @author mazhen
 * 注:AOP ,內部方法之間互相呼叫時,如果是this.xxx()這形式,不會觸發AOP攔截,可能會
 * 導致無法決定資料庫是走寫庫還是讀庫
 * 方法:
 * 為了觸發AOP的攔截,呼叫內部方法時,需要特殊處理下,看方法getService()
 */
@Service
public class CdkeyServiceImpl implements CdkeyService {
	
	private final static Logger logger = LoggerFactory.getLogger(CdkeyParameterServiceImpl.class);
	
	@Autowired
	private CdkeyMapper cdkeyMapper;

	@Override
	@WriteDataSource(description="WRITE")
	public int insertCdkey(Cdkey cdkey) {
		return cdkeyMapper.insert(cdkey);
	}
	
	@Override
	@ReadDataSource(description="READ")
	public Cdkey queryByCdkeyId(Long cdkeyId) {
		return cdkeyMapper.selectByPrimaryKey(cdkeyId);
	}
	
	@Override
	@WriteDataSource(description="WRITE")
	public int deleteByCdkeyId(Long cdkeyId) {
		return cdkeyMapper.deleteByPrimaryKey(cdkeyId);
	}

	@Override
	@WriteDataSource(description="WRITE")
	public int updateCdkey(Cdkey cdkey) {
		return cdkeyMapper.updateByPrimaryKey(cdkey);
	}

	@Override
	@ReadDataSource(description="READ")
	public List<Cdkey> queryAll() {
		return cdkeyMapper.selectAll();
	}

	@Override
	@ReadDataSource(description="READ")
	public Cdkey queryCdkeyByIsUsage(String isUsage) {
		return cdkeyMapper.selectCdkeyByIsUsage(isUsage);
	}

	@Override
	@ReadDataSource(description="READ")
	public Cdkey queryCdkeyByOrderCode(String orderCode) {
		return cdkeyMapper.selectCdkeyByOrderCode(orderCode);
	}

	@Override
	@Transactional
	public String updateCdkeyIsusage(JSONObject jsonObject) {
		String cdkeyStr = null;
		
		Cdkey cdkey = getService().queryCdkeyByIsUsage("0");
	    if (null == cdkey) {
	    	logger.info("兌換碼已用完");
	    	return cdkeyStr;
	    }
		
		cdkey.setOrderCode(jsonObject.getString("orderCode"));
		cdkey.setIsusage(jsonObject.getString("telephone"));
		cdkey.setLastTime(RandomTool.getMillisecond());
	
		
		try {
			if (getService().updateCdkey(cdkey) > 0) {
				cdkeyStr = cdkey.getCdkey();
			}
		} catch (Exception e) {
			logger.info("異常資訊:"+e);
		}
		return cdkeyStr;
	}
	
	private CdkeyServiceImpl getService(){
		return SpringContextUtil.getBean(this.getClass());
	}


}

15、SpringContextUtil.java

package com.wocloud.arch.ssm.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringContextUtil implements ApplicationContextAware{

	private static ApplicationContext applicationContext = null;
	
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		if(SpringContextUtil.applicationContext == null){
			SpringContextUtil.applicationContext = applicationContext;
		}
		
	}

	public static ApplicationContext getApplicationContext() {
		return applicationContext;
	}

	
	public static Object getBean(String name){
		return getApplicationContext().getBean(name);
	}
	
	public static <T> T getBean(Class<T> clazz){
		return getApplicationContext().getBean(clazz);
	}
	
}

16、寫一個Controller進行驗證:

/**
 * 
 */
package com.wocloud.arch.ssm.controller;

import java.io.IOException;
import java.security.DigestException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;


import com.wocloud.arch.ssm.model.CdkeyParameter;
import com.wocloud.arch.ssm.responseResult.ResponseData;
import com.wocloud.arch.ssm.responseResult.ResponseResult;
import com.wocloud.arch.ssm.service.CdkeyParameterService;
import com.wocloud.arch.ssm.service.CdkeyService;

import net.sf.json.JSONObject;


/**
 * @author mazhen
 * 兌換碼請求介面
 */
@RestController
public class CdkeyController {
	/**
	 * 引入日誌,注意都是"org.slf4j"包下
	 */
	private final static Logger logger = LoggerFactory.getLogger(CdkeyController.class);
  
    @Autowired
    private CdkeyParameterService cdkeyParameterService;
    @Autowired
    private CdkeyService cdkeyService;
    
	/*
	 * 兌換碼post請求
	 * @param HttpServletRequest request
	 * @result ResponseResult
	 */
	@RequestMapping(value = "cdkeySender",method = RequestMethod.POST)
	public ResponseData cdkeySender(HttpServletRequest request) {
		logger.info("開始接收合作伙伴的引數");
		Map<String, Object> map = new HashMap<>();
		JSONObject jsonObjectData = null;
		JSONObject jsonObject = null;
		
		try {
			jsonObjectData = ParameterUtil.getParameters(request);
		} catch (IOException e) {
			e.printStackTrace();
			logger.error("介面獲取引數異常:"+e);
			return responseDataFromContent(
					"3",
					"failure",
					null,
					"介面異常");
		}

		/*
		 * 校驗json包體完整性
		 */
		logger.info("從合作伙伴獲取到的jsonObjectData:"+jsonObjectData);
		if (null == jsonObjectData || ("").equals(jsonObjectData) || !jsonObjectData.containsKey("data")) {
			logger.error("介面獲取引數失敗");
			return responseDataFromContent(
					"4",
					"failure",
					null,
					"介面獲取引數失敗");
		}
		
		logger.info(jsonObjectData.getString("data"));
		jsonObject = JSONObject.fromObject(jsonObjectData.getString("data"));
		String telePhone = jsonObject.getString("telephone");
		
		
		try {
				return updateCdkey(jsonObject);
		} catch (DigestException e) {
			e.printStackTrace();
			logger.error("介面簽名演算法異常:"+e);
			return responseDataFromContent(
					"5",
					"failure",
					null,
					"介面簽名演算法異常");
		}
	}

	/*
	 * 處理Response資料
	 */
	private ResponseData responseDataFromContent(final String code, String message, String telePhone, String detail) {
		ResponseData responseData = new ResponseData();
		ResponseResult result = new ResponseResult();
		result.setCode(code);
		result.setMessage(message);
		if (telePhone != null) {
			result.setTelephone(telePhone);
		}
		result.setDetail(detail);
		responseData.setData(result);
		responseData.setResponseTime(RandomTool.getTimeStamp());
		return responseData;
	}
	
	
	/*
	 * 更新cdkey資料庫
	 */
	public ResponseData updateCdkey(JSONObject jsonObject) {
        ResponseData responseData = null;
			
		String cdkeyStr = cdkeyService.updateCdkeyIsusage(jsonObject);
        
        if (cdkeyStr == null) {
			logger.info("cdkey表更新失敗,簡訊傳送失敗!");
			responseData = responseDataFromContent(
						"11",
						"failure",
						jsonObject.getString("telephone"),
						"資料處理失敗");
	   } else {
           logger.info("cdkey表更新成功,簡訊傳送成功!");
		   responseData = responseDataFromContent(
							"0",
							"success",
							jsonObject.getString("telephone"),
							"請求成功")
       }
       return responseData;
						
	}

}

17、ResponseResult.java

/**
 * 
 */
package com.wocloud.arch.ssm.responseResult;

/**
 * @author mazhen
 *
 */
public class ResponseData {
	
    private ResponseResult data;
    
    private String responseTime;

	public ResponseResult getData() {
		return data;
	}

	public void setData(ResponseResult data) {
		this.data = data;
	}

	public String getResponseTime() {
		return responseTime;
	}

	public void setResponseTime(String responseTime) {
		this.responseTime = responseTime;
	}
}

18、ResponseData.java

/**
 * 
 */
package com.wocloud.arch.ssm.responseResult;

/**
 * @author mazhen
 *
 */
public class ResponseResult {
    private String code;
	
	private String message;
	
	private String telephone;
	
	private String detail;

	public String getCode() {
		return code;
	}

	public void setCode(String code) {
		this.code = code;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}
		
	
	public String getTelephone() {
		return telephone;
	}

	public void setTelephone(String telephone) {
		this.telephone = telephone;
	}

	public String getDetail() {
		return detail;
	}

	public void setDetail(String detail) {
		this.detail = detail;
	}

	public ResponseResult(String code,String message,String telephone,String detail){
		super();
		this.code = code;
		this.message = message;
		this.telephone = telephone;
		this.detail = detail;
	}
	public ResponseResult(){
		
	}
}

19、ParameterUtil.java

/**
 * 
 */
package com.wocloud.arch.ssm.utils.common;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.http.HttpServletRequest;

import net.sf.json.JSONObject;

/**
 * @author mazhen
 * 獲取post請求中的body內容
 */
public class ParameterUtil {

    public static JSONObject getParameters(HttpServletRequest request) throws IOException {
        //從request中以"UTF-8"形式獲取輸入流,避免中文亂碼
        BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream(),"UTF-8"));
        StringBuffer sb = new StringBuffer("");
        String temp;
        //一行一行讀取並放入到StringBuffer中
        while((temp = br.readLine()) != null){
            sb.append(temp);
        }
        br.close();
        String acceptjson = sb.toString();
        JSONObject jo = null;
        //把String轉成JSON物件
        if (acceptjson != "") {
            jo = JSONObject.fromObject(acceptjson);
        }
        return jo;
    }

}

 最後,使用apache bench進行測試,事務的生效如圖所示: