※MySQL(InnoDB)での検証結果です。他のDBでは異なる動作になる可能性があります。
トランザクション分離レベルがデフォルト(REPEATABLE READ※)の場合
※デフォルトのトランザクション分離レベルはDBによって異なります。MySQL(InnoDB)のデフォルトはREPEATABLE READですが、その他のDBの多くはREAD COMMITTEDのようです。
@Transactional
public void updateItem(int itemId) {
Item item = itemMapper.getById(itemId);
item.setNum(item.getNum() + 1);
itemMapper.update(item);
(何かの処理)
...
}
このメソッドが順番に2回呼ばれた場合、item.numが合計+2される。この動作は他のパターンでも共通。
このメソッドがほぼ同時に2回呼ばれた場合、後に呼ばれた方はitemMapper.update(item);
の直前で停止し、先に呼ばれたメソッドが終わるまで待つ。そのため、どちらのメソッド呼び出しもnumに初期状態から+1された状態のデータでupdateされることになる。
これを順次実行と同様の結果にするには、例えばupdateの条件式にpriceが元の値と同一であるという条件を入れ(楽観的ロック)、updateに失敗したらメソッドの実行をリトライするなどの手を加える必要がある。
トランザクション分離レベルがSERIALIZABLEの場合
@Transactional(isolation=Isolation.SERIALIZABLE)
public void updateItem(int itemId) {
Item item = itemMapper.getById(itemId);
item.setNum(item.getNum() + 1);
itemMapper.update(item);
(何かの処理)
...
}
このメソッドがほぼ同時に2回呼ばれた場合、後に呼ばれた方はメソッド呼び出し時点で停止し、先に呼ばれたメソッドが終わるまで待つ。そのため、先に呼ばれた方でnumに初期状態から+1され、後に呼ばれた方はその値を元にさらに+1するので、順番に2回呼ばれたのと同じ動作となる。
ネストされたトランザクションの外側がデフォルト(REPEATABLE READ)、内側がSERIALIZABLEの場合
@Transactional
public void updateItem(int itemId) {
updateItemInner(itemId);
}
@Transactional(isolation=Isolation.SERIALIZABLE)
private void updateItemInner(int itemId) {
Item item = itemMapper.getById(itemId);
item.setNum(item.getNum() + 1);
itemMapper.update(item);
(何かの処理)
...
}
この場合の動作はデフォルト(REPEATABLE READ)になる!つまり、このメソッドがほぼ同時に2回呼ばれた場合、numは初期状態から+1された状態になる。トランザクション分離レベルにSERIALIZABLEを指定したからといってそれだけでSERIALIZABLEの動作になることが保証されているわけではないということなので注意。
当然ながら外側を非トランザクションにすればSERIALIZABLEの動作になる。
ネストされたトランザクションの外側がSERIALIZABLEの場合
@Transactional(isolation=Isolation.SERIALIZABLE)
public void updateItem(int itemId) {
updateItemInner(itemId);
}
@Transactional
private void updateItemInner(int itemId) {
Item item = itemMapper.getById(itemId);
item.setNum(item.getNum() + 1);
itemMapper.update(item);
(何かの処理)
...
}
この場合は内側がどうあれ、SERIALIZABLEの動作になる。つまり、このメソッドがほぼ同時に2回呼ばれた場合、numは初期状態から+2された状態になる。