这个功能是要产生一个顺序增长的流水号。简化的代码如下:
@Transactional(readOnly=false, propagation=Propagation.REQUIRED)
public RunningNumber getNextNumber(String runningType){
RunningNumberType type = config.getType(runningType);
RunningNumber runningNumber = null;
synchronized (getClass()) {
do {
runningNumber = dao.queryRunningNumber(type.getType());
if (runningNumber == null) {
runningNumber = new RunningNumber();
runningNumber.setRunningType(type.getType());
runningNumber.setRunningNumber("1");
try {
dao.insert(runningNumber);
} catch (Exception e) {
runningNumber = null;
}
} else {
String number = runningNumber.getRunningNumber();
runningNumber.setRunningNumber(""+(new Long(number) + 1));
if (dao.update(runningNumber) == 0) {
runningNumber = null;
}
}
} while (runningNumber == null);
}
runningNumber.setRunningDate(runningNumber.getRunningDate().trim());
return runningNumber;
}
这个逻辑也很简单,就是获取一个新的流水号。如果失败了就再次尝试直到成功。
为了解决多服务器时的冲突问题,流水号表使用了版本号控制的乐观锁。即修改时运行的SQL如下:
update T_RUNNINGNUMBER set version = version + 1, running_Type=?, running_Number=? where ID=? and version=?
这样如果有修改冲突情况下,修改会失败。
上面的程序逻辑看起来应该没问题。为了避免同一个VM中多线程间的冲突,还synchronize了关键代码。但是在多线程访问的情况下,这段程序会陷入死循环。为什么会死循环呢?我百思不得其解。
最后我终于想明白了原因。这是事务隔离导致的问题。Mysql数据库缺省的事务隔离是REPEATABLE_READ,即同一个事务中多次读同一条记录,读出来的值是一样的。如果线程A和B同时调用getNextNumber方法,假设这时目标记录的version是72,再假设线程A先进入了同步代码段,成功地生成了一个新流水号,然后退出这个方法,结束了事务。这时数据库里目标记录的version变成了73。然后线程B进入了同步代码段,但是由于事务隔离是REPEATABLE_READ,导致线程B读到的version始终是72,也就是说线程B永远都不可能成功地生成新流水号,将陷入死循环。
原因明白了,解决方法也就清楚了。降低事务的隔离级别,设为READ_COMMITTED,即读取已提交事务,就可解决这个问题。同时事务传递也要改为REQUIRES_NEW。为什么必须改为REQUIRES_NEW呢?如果是REQUIRED,那么这个方法有可能被另外一个定义事务的方法调用,此方法定义的事务隔离级别就不起作用了。为了保证正确的隔离级别,这里必须启用一个新的事物。
@Transactional(readOnly=false, propagation=Propagation.REQUIRES_NEW, isolation=Isolation.READ_COMMITTED)
public String getNextNumber(String runningType){
这个bug也提醒了我们,事务的缺省隔离级别并非放之四海而皆准的,有时候还是需要根据具体的业务场景设置合适的隔离级别的。