概要
AtomicLong は単一 JVM 内で完結する採番には最適ですが、複数サーバー(マルチ JVM)から同じ連番体系で採番する場合は使えません。このような場面では DB の採番テーブルに現在値を保持し、UPDATE 文の行ロックで排他制御を行う方式が広く採用されています。この記事では、UPDATE で直接インクリメントする方式と、SELECT FOR UPDATE で行をロックしてから更新する方式の2つを JDBC で実装します。それぞれのトランザクション制御の違い、デッドロックのリスク、コミットのタイミングといった現場で実際に判断が必要になるポイントを整理します。H2 インメモリ DB で動作確認できる完結したコードを示すため、手元ですぐに試せます。
使いどころ
複数の AP サーバーから共通の注文番号体系で採番し、番号の重複を DB の行ロックで防ぐ
バッチサーバーと Web サーバーが同じ請求番号を使う構成で、採番テーブルを共有して一意性を担保する
用途別(注文・請求・出荷)に採番行を分けて管理し、それぞれ独立した連番を払い出す
コード例
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class DbAtomicCounterDemo {
/** UPDATE 方式: 行を直接インクリメントして新しい値を取得 */
public static long nextVal(Connection conn, String seqName)
throws SQLException {
var updateSql = "UPDATE seq_table SET current_val = current_val + 1 "
+ "WHERE seq_name = ?";
var selectSql = "SELECT current_val FROM seq_table WHERE seq_name = ?";
try (var upd = conn.prepareStatement(updateSql);
var sel = conn.prepareStatement(selectSql)) {
upd.setString(1, seqName);
if (upd.executeUpdate() == 0) {
throw new SQLException("採番行が見つかりません: " + seqName);
}
sel.setString(1, seqName);
try (var rs = sel.executeQuery()) {
if (rs.next()) {
return rs.getLong("current_val");
}
throw new SQLException("採番値の取得に失敗: " + seqName);
}
}
}
/** SELECT FOR UPDATE 方式: 行ロック後に読み取り・更新 */
public static long nextValWithLock(Connection conn, String seqName)
throws SQLException {
var selectForUpdate = """
SELECT current_val FROM seq_table
WHERE seq_name = ? FOR UPDATE
""";
var updateSql = """
UPDATE seq_table SET current_val = ?
WHERE seq_name = ?
""";
try (var sel = conn.prepareStatement(selectForUpdate);
var upd = conn.prepareStatement(updateSql)) {
sel.setString(1, seqName);
long current;
try (var rs = sel.executeQuery()) {
if (!rs.next()) {
throw new SQLException("採番行が見つかりません: " + seqName);
}
current = rs.getLong("current_val");
}
var next = current + 1;
upd.setLong(1, next);
upd.setString(2, seqName);
upd.executeUpdate();
return next;
}
}
/** トランザクションヘルパー */
@FunctionalInterface
interface TxWork<T> {
T execute(Connection conn) throws SQLException;
}
public static <T> T runInTransaction(Connection conn, TxWork<T> work)
throws SQLException {
conn.setAutoCommit(false);
try {
T result = work.execute(conn);
conn.commit();
return result;
} catch (SQLException e) {
conn.rollback();
throw e;
}
}
public static void main(String[] args) throws Exception {
var url = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1";
try (var conn = DriverManager.getConnection(url, "sa", "")) {
// テーブル準備
try (var st = conn.createStatement()) {
st.execute("""
CREATE TABLE IF NOT EXISTS seq_table (
seq_name VARCHAR(64) PRIMARY KEY,
current_val BIGINT NOT NULL DEFAULT 0
)
""");
}
conn.setAutoCommit(false);
try (var ps = conn.prepareStatement(
"INSERT INTO seq_table (seq_name, current_val) VALUES (?, ?)")) {
ps.setString(1, "ORDER_SEQ");
ps.setLong(2, 10000);
ps.executeUpdate();
}
conn.commit();
// 採番テーブル方式で5回採番
System.out.println("=== UPDATE 方式 ===");
for (var i = 0; i < 5; i++) {
var val = runInTransaction(conn,
c -> nextVal(c, "ORDER_SEQ"));
System.out.println("注文番号: ORD-" + val);
}
}
}
}Version Coverage
var で変数宣言を簡潔にし、テキストブロック(""")で複数行 SQL を読みやすく書ける。トランザクションヘルパーを FunctionalInterface で汎用化できる。
// Java 17: テキストブロックで SQL を読みやすく
var selectForUpdate = """
SELECT current_val FROM seq_table
WHERE seq_name = ? FOR UPDATE
""";
try (var sel = conn.prepareStatement(selectForUpdate)) {
sel.setString(1, seqName);
try (var rs = sel.executeQuery()) {
// ...
}
}Library Comparison
注意点
autoCommit が true のままだと UPDATE 直後にロックが解放され、SELECT で取得する値が他トランザクションの値になる可能性がある。必ず autoCommit=false で使うこと
SELECT FOR UPDATE 方式は、同じテーブルの複数行を異なる順序でロックするとデッドロックのリスクがある。採番テーブルでは1行ずつ操作するのが原則
採番テーブルへのアクセスは短時間で commit して行ロックを開放すること。長いトランザクションに含めると後続のリクエストが待たされる
H2 のインメモリ DB はテスト用。本番では MySQL / PostgreSQL 等のドライバを CLASSPATH に追加し、接続文字列を変更する
DB シーケンス(PostgreSQL の SEQUENCE、MySQL の AUTO_INCREMENT)が使える場合は採番テーブルより簡潔。テーブル方式は DB 非依存や用途別管理が必要な場合に選ぶ
FAQ
UPDATE 方式のほうが単純で安全です。SELECT FOR UPDATE は更新前の値を読み取る必要がある場合に使いますが、デッドロックのリスクが増えるため、採番だけなら UPDATE + SELECT の2文で済ませるのが無難です。
一度に複数番号を取得するバッチ採番(例: 100番分をまとめて UPDATE)が有効です。アプリ側でプールしておき、使い切ったら次のバッチを取得する方式で DB アクセス頻度を減らせます。
起動時に採番テーブルの current_val を SELECT し、その値で AtomicLong を初期化します。DB 採番と JVM 内採番を併用する場合はバッチ取得方式にして、DB 側を正とするのが安全です。