MySQLMaxValueIncrementer supports "useNewConnection" mode for INNODB tables

Issue: SPR-15107
This commit is contained in:
Juergen Hoeller 2017-01-12 22:53:10 +01:00
parent 5982f732b4
commit cc53d597c0
1 changed files with 76 additions and 14 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -33,14 +33,14 @@ import org.springframework.jdbc.support.JdbcUtils;
* key column should <i>NOT</i> be auto-increment, as the sequence table does the job.
*
* <p>The sequence is kept in a table; there should be one sequence table per
* table that needs an auto-generated key. The table type of the sequence table
* should be MyISAM so the sequences are allocated without regard to any
* transactions that might be in progress.
* table that needs an auto-generated key. The storage engine used by the sequence table
* can be MYISAM or INNODB since the sequences are allocated using a separate connection
* without being affected by any other transactions that might be in progress.
*
* <p>Example:
*
* <pre class="code">create table tab (id int unsigned not null primary key, text varchar(100));
* create table tab_sequence (value int not null) type=MYISAM;
* create table tab_sequence (value int not null);
* insert into tab_sequence values(0);</pre>
*
* If "cacheSize" is set, the intermediate values are served without querying the
@ -48,6 +48,10 @@ import org.springframework.jdbc.support.JdbcUtils;
* is rolled back, the unused values will never be served. The maximum hole size in
* numbering is consequently the value of cacheSize.
*
* <p>It is possible to avoid acquiring a new connection for the incrementer by setting the
* "useNewConnection" property to false. In this case you <i>MUST</i> use a non-transactional
* storage engine like MYISAM when defining the incrementer table.
*
* @author Jean-Pierre Pawlak
* @author Thomas Risberg
* @author Juergen Hoeller
@ -63,6 +67,9 @@ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer
/** The max id to serve */
private long maxId = 0;
/** Whether or not to use a new connection for the incrementer */
private boolean useNewConnection = false;
/**
* Default constructor for bean property style usage.
@ -76,7 +83,7 @@ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer
/**
* Convenience constructor.
* @param dataSource the DataSource to use
* @param incrementerName the name of the sequence/table to use
* @param incrementerName the name of the sequence table to use
* @param columnName the name of the column in the sequence table to use
*/
public MySQLMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) {
@ -84,23 +91,61 @@ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer
}
/**
* Set whether to use a new connection for the incrementer.
* <p>{@code true} is necessary to support transactional storage engines,
* using an isolated separate transaction for the increment operation.
* {@code false} is sufficient if the storage engine of the sequence table
* is non-transactional (like MYISAM), avoiding the effort of acquiring an
* extra {@code Connection} for the increment operation.
* <p>Default is {@code false} in the Spring Framework 4.3.x line.
* @since 4.3.6
* @see DataSource#getConnection()
*/
public void setUseNewConnection(boolean useNewConnection) {
this.useNewConnection = useNewConnection;
}
@Override
protected synchronized long getNextKey() throws DataAccessException {
if (this.maxId == this.nextId) {
/*
* Need to use straight JDBC code because we need to make sure that the insert and select
* are performed on the same connection (otherwise we can't be sure that last_insert_id()
* returned the correct value)
* If useNewConnection is true, then we obtain a non-managed connection so our modifications
* are handled in a separate transaction. If it is false, then we use the current transaction's
* connection relying on the use of a non-transactional storage engine like MYISAM for the
* incrementer table. We also use straight JDBC code because we need to make sure that the insert
* and select are performed on the same connection (otherwise we can't be sure that last_insert_id()
* returned the correct value).
*/
Connection con = DataSourceUtils.getConnection(getDataSource());
Connection con = null;
Statement stmt = null;
boolean mustRestoreAutoCommit = false;
try {
if (this.useNewConnection) {
con = getDataSource().getConnection();
if (con.getAutoCommit()) {
mustRestoreAutoCommit = true;
con.setAutoCommit(false);
}
}
else {
con = DataSourceUtils.getConnection(getDataSource());
}
stmt = con.createStatement();
DataSourceUtils.applyTransactionTimeout(stmt, getDataSource());
if (!this.useNewConnection) {
DataSourceUtils.applyTransactionTimeout(stmt, getDataSource());
}
// Increment the sequence column...
String columnName = getColumnName();
stmt.executeUpdate("update "+ getIncrementerName() + " set " + columnName +
" = last_insert_id(" + columnName + " + " + getCacheSize() + ")");
try {
stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName +
" = last_insert_id(" + columnName + " + " + getCacheSize() + ")");
}
catch (SQLException ex) {
throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " +
getIncrementerName() + " sequence table", ex);
}
// Retrieve the new max of the sequence column...
ResultSet rs = stmt.executeQuery(VALUE_SQL);
try {
@ -119,7 +164,24 @@ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer
}
finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
if (con != null) {
if (this.useNewConnection) {
try {
con.commit();
if (mustRestoreAutoCommit) {
con.setAutoCommit(true);
}
}
catch (SQLException ignore) {
throw new DataAccessResourceFailureException(
"Unable to commit new sequence value changes for " + getIncrementerName());
}
JdbcUtils.closeConnection(con);
}
else {
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
}
}
else {