併發自定義主鍵生成策略
前言
在專案開發中,我遇到一個需求.就是要生成自定義的主鍵.
主鍵的格式為:字首 + 當天日期 + 自增長序列號
自增長序列號
: 這個序列號是每天從0或1開始自增長的
在瞭解完需要之後,我做了簡單的分析之後就馬上進入編碼模式,這裡我使用的是mysql資料,全部程式碼已經上傳到github,有需要的同學自行下載
目錄結構(專案管理工具用的maven)
- src
- main
- java
- com.kco.bean.SequenceNumberBean
- com.kco.dao.SequenceNumberDao
- com.kco.Enum.SequenceNumberEnum
- com.kco.service.SequenceNumberService
- com.kco.service.SequenceNumberServiceImpl
- resources
- META-INF/mybatis/mapper/SequenceNumberDao.xml
- META-INF/mybatis/sql-map-config.xml
- META-INF/spring/spring-base-jdbc.xml
- log4j.properties
- java
- test
- java
- com.kco.TestSequenceNumberService
- java
- main
- pom.xml
編碼
1, 首先建立一個maven工程,配置一下jar依賴 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kco</groupId>
<artifactId >mytestcode</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.3</version>
</dependency>
<!-- test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
<version>4.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.12</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.7</version>
</dependency>
<!-- spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.8</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.8.8</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.8</version>
</dependency>
<!--spring mybatis 整合-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.13</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.13</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>runtime</scope>
<version>1.7.13</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>tbl</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
</project>
2., 初始化資料庫
CREATE TABLE PUB_SEQUENCE_NUMBER(
prefix VARCHAR(10) NOT NULL PRIMARY KEY COMMENT '字首(主鍵)',
NAME VARCHAR(30) NOT NULL COMMENT '描述',
toda
y char(8) NOT NULL COMMENT '當天日期',
minNum INTEGER NOT NULL DEFAULT 0 COMMENT '序列號最小號碼',
currentNum INTEGER NOT NULL DEFAULT 0 COMMENT '當前序列號',
numLength INTEGER NOT NULL DEFAULT 8 COMMENT '序列號長度'
);
3, 整合spring和mybatis,建立執行緒池和事務切面等 spring-base-jdbc.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"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:annotation-config/>
<context:component-scan base-package="com.kco" />
<!-- 配置config的資料庫 -->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///test?useUnicode=yes&characterEncoding=utf8&allowMultiQueries=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="minIdle" value="10"/>
<property name="maxIdle" value="10"/>
<property name="maxWaitMillis" value="-1"/>
<property name="maxTotal" value="10"/>
</bean>
<!-- spring和MyBatis完美整合-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" >
<property name="dataSource" ref="dataSource" />
<property name="configLocation"
value="classpath:META-INF/mybatis/sql-map-config.xml" />
<property name="mapperLocations" value="classpath*:META-INF/mybatis/mapper/*Dao.xml" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" id="mapperScannerConfigurer">
<property name="annotationClass" value="org.springframework.stereotype.Repository"/>
<property name="basePackage" value="com.kco.dao"/>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* *..*ServiceImpl*.*(..))"/>
</aop:config>
</beans>
4, 配置mybatis sql-map-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<properties>
<property name="dialect" value="mysql"/>
<property name="pageSqlId" value=".*Page$"/>
</properties>
<settings>
<setting name="cacheEnabled" value="false"/>
<setting name="lazyLoadingEnabled" value="false"/>
</settings>
</configuration>
5, 因為要生成自定義主鍵一般在開發階段就已經能確定,所以我這裡使用了一個列舉類來管理所有的自定義主鍵 com.kco.Enum.SequenceNumberEnum
package com.kco.Enum;
import com.kco.bean.SequenceNumberBean;
/**
* com.kco.Enum
* Created by swlv on 2016/10/25.
*/
public enum SequenceNumberEnum {
GD(new SequenceNumberBean("GD","工單主鍵生成策略", 1, 1, 8));
private SequenceNumberBean sequenceNumberBean;
SequenceNumberEnum(SequenceNumberBean sequenceNumberBean){
this.sequenceNumberBean = sequenceNumberBean;
}
public SequenceNumberBean getSequenceNumberBean() {
return sequenceNumberBean;
}
}
6, 資料庫PUB_SEQUENCE_NUMBER
對應的bean類 com.kco.bean.SequenceNumberBean
package com.kco.bean;
/**
* 主鍵生成策略的bean
* com.kco.bean
* Created by swlv on 2016/10/25.
*/
public class SequenceNumberBean {
private String prefix;
private String name;
private String today;
private int minNum;
private int currentNum;
private int numLength;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getToday() {
return today;
}
public void setToday(String today) {
this.today = today;
}
public int getMinNum() {
return minNum;
}
public void setMinNum(int minNum) {
this.minNum = minNum;
}
public int getCurrentNum() {
return currentNum;
}
public void setCurrentNum(int currentNum) {
this.currentNum = currentNum;
}
public int getNumLength() {
return numLength;
}
public void setNumLength(int numLength) {
this.numLength = numLength;
}
public SequenceNumberBean() {
}
public SequenceNumberBean(String prefix, String name, int minNum, int currentNum, int numLength) {
this.prefix = prefix;
this.name = name;
this.minNum = minNum;
this.currentNum = currentNum;
this.numLength = numLength;
}
@Override
public String toString() {
return "SequenceNumberBean{" +
"prefix='" + prefix + '\'' +
", name='" + name + '\'' +
", today='" + today + '\'' +
", minNum=" + minNum +
", currentNum=" + currentNum +
", numLength=" + numLength +
'}';
}
}
7, 服務層的介面類以及實現 com.kco.service.SequenceNumberService
package com.kco.service;
import com.kco.Enum.SequenceNumberEnum;
/**
* com.kco.service
* Created by swlv on 2016/10/25.
*/
public interface SequenceNumberService {
/**
* 生成一個主鍵
* @param sequenceNumberEnum 主鍵生成型別
* @return 返回一個生成的主鍵
*/
String newSequenceNumber(SequenceNumberEnum sequenceNumberEnum);
}
com.kco.service.SequenceNumberServiceImpl
package com.kco.service;
import com.kco.Enum.SequenceNumberEnum;
import com.kco.bean.SequenceNumberBean;
import com.kco.dao.SequenceNumberDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* com.kco.service
* Created by swlv on 2016/10/25.
*/
@Service
public class SequenceNumberServiceImpl implements SequenceNumberService{
@Resource
private SequenceNumberDao sequenceNumberDao;
@Override
public synchronized String newSequenceNumber(SequenceNumberEnum sequenceNumberEnum) {
if (sequenceNumberEnum == null){
return null;
}
try {
SequenceNumberBean sequenceNumberBean = sequenceNumberDao.newSequenceNumber(sequenceNumberEnum.getSequenceNumberBean().getPrefix());
Thread.sleep(100);
if (sequenceNumberBean == null){
sequenceNumberBean = sequenceNumberEnum.getSequenceNumberBean();
sequenceNumberBean.setToday(sequenceNumberDao.getToday());
}
Thread.sleep(100);
sequenceNumberDao.updateSequenceNumber(sequenceNumberBean);
Thread.sleep(100);
return String.format("%s%6s%08d",
sequenceNumberBean.getPrefix(), sequenceNumberBean.getToday(), sequenceNumberBean.getCurrentNum());
} catch (InterruptedException e) {
e.printStackTrace();
return "";
}
}
}
8, 資料庫dao層介面 com.kco.dao.SequenceNumberDao
package com.kco.dao;
import com.kco.bean.SequenceNumberBean;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
* com.kco.dao
* Created by swlv on 2016/10/25.
*/
@Repository
public interface SequenceNumberDao {
/**
* 根據字首生成一個序列號資訊
* @param prefix 字首
* @return 新的序列號資訊
*/
SequenceNumberBean newSequenceNumber(@Param("prefix") String prefix);
/**
* 將生成的序列號資訊更新到資料庫中
* @param sequenceNumberBean 需要更新資訊
*/
void updateSequenceNumber(@Param("bean") SequenceNumberBean sequenceNumberBean);
/**
* 獲取資料庫當天日期
* @return
*/
String getToday();
}
9., dao層mybatis的實現 SequenceNumberDao.xml
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.kco.dao.SequenceNumberDao">
<resultMap id="sequenceNumberResultMap" type="com.kco.bean.SequenceNumberBean">
<id property="prefix" column="prefix"/>
<result property="name" column="name"/>
<result property="today" column="today"/>
<result property="minNum" column="minNum"/>
<result property="currentNum" column="currentNum"/>
<result property="numLength" column="numLength"/>
</resultMap>
<select id="newSequenceNumber" resultMap="sequenceNumberResultMap">
SELECT prefix,name,today,minNum,currentNum + 1 as currentNum,numLength
FROM PUB_SEQUENCE_NUMBER
WHERE prefix = #{prefix}
AND today = DATE_FORMAT(CURRENT_DATE(),'%Y%m%d')
</select>
<update id="updateSequenceNumber" parameterType="com.kco.bean.SequenceNumberBean">
REPLACE INTO PUB_SEQUENCE_NUMBER(prefix,name,today,minNum,currentNum,numLength)
VALUES(#{bean.prefix}, #{bean.name}, DATE_FORMAT(CURRENT_DATE(),'%Y%m%d'), #{bean.minNum}, #{bean.currentNum},#{bean.numLength})
</update>
<select id="getToday" resultType="string">
SELECT DATE_FORMAT(CURRENT_DATE(),'%Y%m%d')
</select>
</mapper>
10, 編寫測試程式碼(模擬併發測試) com.kco.TestSequenceNumberService
package com.kco;
import com.kco.Enum.SequenceNumberEnum;
import com.kco.service.SequenceNumberService;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
/**
* com.cmbchina.base.service
* Created by swlv on 2016/10/25.
*/
public class TestSequenceNumberService {
public static void main(String[] args) throws InterruptedException {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("META-INF/spring/spring-base-jdbc.xml");
SequenceNumberService sequenceNumberService = applicationContext.getBean(SequenceNumberService.class);
Queue<String> queue = new ArrayBlockingQueue<String>(200);
List<Thread> list = new ArrayList<>();
for (int i = 0;i < 200; i ++){
list.add(new Thread(()->{
String key = sequenceNumberService.newSequenceNumber(SequenceNumberEnum.GD);
queue.add(key);
}));
}
for (Thread thread : list){
thread.start();
}
while (queue.size() != 200);
System.out.println(queue);
}
}
11, 至此整個專案就已經搭建完成
細節解析
在
com.kco.service.SequenceNumberServiceImpl#newSequenceNumber
的實現中在更新當天日期字串時,使用的是sequenceNumberBean.setToday(sequenceNumberDao.getToday());
這是因為伺服器的時間跟資料庫的時間有可能不一致,這裡以資料庫的時間為準
測試
1, 查詢資料庫的資料
SELECT count(1) FROM PUB_SEQUENCE_NUMBER
結果:
count(1) |
---|
0 |
2, 清空資料後,再執行com.kco.TestSequenceNumberService#main
3, 然後查一下資料庫
SELECT * FROM PUB_SEQUENCE_NUMBER
prefix | NAME | today | minNum | currentNum | numLength |
---|---|---|---|---|---|
GD | 工單主鍵生成策略 | 20161026 | 1 | 176 | 8 |
問題出現了,明明我是啟動了200個執行緒在跑,怎麼才生成176個主鍵,檢查程式碼也沒有問題啊,
newSequenceNumber
也加了關鍵字synchronized
.先把這個問題放一下,來回憶一下
synchronized
的用法,
synchronized
是線上程池在訪問共同變數時使其加鎖,達到互斥的效果
共同變數?共同變數?咦,好像有什麼不對勁的地方.newSequenceNumber
使用的都是區域性變數,沒有共同變數啊!不對?是有共同變數,那個共同變數就是資料庫的記錄
除此之外使用synchronized
還有一個問題,這個專案是web專案的一部分,一般在部署web專案都是有幾個單邊,即在不同的PC部署幾個一模一樣的程式碼,再通過叢集訪問的方式隨機訪問其中一臺.那麼這幾個應用其實就是多程序程式,都不再一個JVM中執行,加synchronized
來保證互斥,是不夠的.
4, 既然知道是資料庫同步出現了問題,那麼怎麼保證資料庫同步呢?
上網搜尋一下,發現數據庫有悲觀鎖
和樂觀鎖
解決方法就是在Dao層的實現
<select id="newSequenceNumber" resultMap="sequenceNumberResultMap">
SELECT prefix,name,today,minNum,currentNum + 1 as currentNum,numLength
FROM PUB_SEQUENCE_NUMBER
WHERE prefix = #{prefix}
AND today = DATE_FORMAT(CURRENT_DATE(),'%Y%m%d')
</select>
改為
<select id="newSequenceNumber" resultMap="sequenceNumberResultMap">
SELECT prefix,name,today,minNum,currentNum + 1 as currentNum,numLength
FROM PUB_SEQUENCE_NUMBER
WHERE prefix = #{prefix}
AND today = DATE_FORMAT(CURRENT_DATE(),'%Y%m%d') for UPDATE
</select>
5, 然後再重新查一下資料庫
SELECT * FROM PUB_SEQUENCE_NUMBER
prefix | NAME | today | minNum | currentNum | numLength |
---|---|---|---|---|---|
GD | 工單主鍵生成策略 | 20161026 | 1 | 200 | 8 |
發現問題解決了,這裡是用到了資料庫鎖表的機制來實現同步.
以後有機會再學一下資料庫同步,
悲觀鎖
和樂觀鎖
.因為這個我還不太熟悉,這裡就不賣弄了.
6., 修改一下 com.kco.Enum.SequenceNumberEnum
再增加一個自定義主鍵GDD(new SequenceNumberBean("GDD","工單主鍵生成策略", 1, 1, 8))
7., 清空資料,然後執行com.kco.TestSequenceNumberService#main
8, 修改com.kco.TestSequenceNumberService2#main
將String key = sequenceNumberService.newSequenceNumber(SequenceNumberEnum.GD);
改為String key = sequenceNumberService.newSequenceNumber(SequenceNumberEnum.GDD);
再次執行com.kco.TestSequenceNumberService#main
, 這是用來模擬兩個程式同時訪問資料的情況
9, 查詢資料
SELECT * FROM PUB_SEQUENCE_NUMBER
prefix | NAME | today | minNum | currentNum | numLength |
---|---|---|---|---|---|
GD | 工單主鍵生成策略 | 20161026 | 1 | 200 | 8 |
GDD | 工單主鍵生成策略 | 20161026 | 1 | 200 | 8 |
資料正常
10., 清空資料,連續執行兩次com.kco.TestSequenceNumberService2#main
模擬多程序對生成同一個自定義主鍵
11, 查詢結果
SELECT * FROM PUB_SEQUENCE_NUMBER
prefix | NAME | today | minNum | currentNum | numLength |
---|---|---|---|---|---|
GDD | 工單主鍵生成策略 | 20161026 | 1 | 400 | 8 |
至此, 全部搞定
遺留問題
- 資料庫
悲觀鎖
和樂觀鎖
具體是怎麼樣的.有什麼區別? for update
鎖表對效能有沒有什麼影響,除了效能,還有其他隱藏的隱患?
這幾個問題就留給讀者(還有我自己)自行查資料研究了
打賞
如果覺得我的文章寫的還過得去的話,有錢就捧個錢場,沒錢給我捧個人場(幫我點贊或推薦一下)