写几个例子, 来示范一下 数据库事务 的误区。
下面的例子场景为转账,使用mysql,innodb, 缺省read committed隔离级别,spring+hibernate. 不考虑边界条件,如余额不足等。
我只要声明了 事务,就万事大吉了么?
先给一个entity的代码
@Entity public class Money { private Long id;private Long balance; private long version; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long getId() { return id; } public void setId(Long uid) { this.id = uid; } public Long getBalance() { return balance; } public void setBalance(Long balance) { this.balance = balance; }
}
在给一个service的代码,一切从简,没有dao.
@Service public class MoneyService { @Autowired private HibernateTemplate template;@Transactional public void transfer(Long from, Long to, Long amount) { Money fromAcount = template.load(Money.class, from); Money toAccount = template.load(Money.class, to); fromAcount.setBalance(fromAcount.getBalance() - amount); // just for test try { Thread.sleep(1000l); } catch (InterruptedException e) { e.printStackTrace(); } // //////////////// toAccount.setBalance(toAccount.getBalance() + amount); template.save(fromAcount); template.save(toAccount); }
}
你觉得这段代码有问题么?同时起2个线程,一个让用户A(余额100)给C(余额100)转账10,另一个让用户B(余额100)给用户C(余额100)转账10, 结果是A(余额90),B(余额90), C(余额110), 丢失了10块钱。。。。。。
这里面可能引发另外一个疑问,数据库教程书也是这么用的啊,设了事务,人家转账咋就没问题。
好吧,让我们把数据库教程里的sql代码翻译成同样的hibernate代码。
@Transactional public void transfer2(final Long from, final Long to, final Long amount) {template.execute(new HibernateCallback<Void>() { public Void doInHibernate(Session session) throws HibernateException, SQLException { SQLQuery q = session .createSQLQuery("update money set balance = (balance - ?) where id = ?"); q.setLong(0, amount); q.setLong(1, from); q.executeUpdate(); return null; } }); // just for test try { Thread.sleep(1000l); } catch (InterruptedException e) { e.printStackTrace(); } // //////////////// template.execute(new HibernateCallback<Void>() { public Void doInHibernate(Session session) throws HibernateException, SQLException { SQLQuery q = session .createSQLQuery("update money set balance = (balance + ?) where id = ?"); q.setLong(0, amount); q.setLong(1, to); q.executeUpdate(); return null; } }); }</pre>
这里需要指出,事务保证的只是sql的完整性,如果是程序搞砸的,那事务也爱莫能助。
transfer产生的sql代码为
select * from money where id=A.id;
select * from money where id=C.id;
update money set balance=90 where id=A.id;update money set balance=110 where id=C.id;
和
select * from money where id=B.id;
select * from money where id=C.id;
update money set balance=90 where id=B.id;update money set balance=110 where id=C.id;
余额90 和 110 都是程序计算出来的,所以事务也爱莫能助。
而transfer2 产生的sql代码为
update money set balance=balance - 10 where id=A.id;update money set balance=balance + 10 where id=C.id;
和update money set balance=balance - 10 where id=B.id;update money set balance=balance + 10 where id=C.id;
数据库事务保证结果是A(余额90),B(余额90), C(余额120)。
问题虽然解决了,但不能只能写sql啊,我还是需要使用hibernate,怎么办呢?这里需要借助悲观锁或乐观锁了。悲观锁:@Transactional public void transfer3(Long from, Long to, Long amount) { Money fromAcount = template.load(Money.class, from, LockMode.PESSIMISTIC_WRITE); Money toAccount = template.load(Money.class, to, LockMode.PESSIMISTIC_WRITE);fromAcount.setBalance(fromAcount.getBalance() - amount); // //////////////// try { Thread.sleep(1000l); } catch (InterruptedException e) { e.printStackTrace(); } // //////////////// toAccount.setBalance(toAccount.getBalance() + amount); template.save(fromAcount); template.save(toAccount); }
这样在进行select的时候,会同时将记录锁住,另外一个线程在对同一条记录进行查询的时候,必须等待锁的释放。需要小心使用悲观锁,不正确的顺序会大大增加数据库死锁的概率。
结果,余额对了, 但你会发现 其中一个线程的 select 花费了 1秒钟,因为它一直在等待 另外一个线程释放锁。
还有一种解决方案是乐观锁:
列一下代码:
package com.wuxudong.entity;import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Version;@Entity
public class Money {
private Long id;private Long balance; private long version; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long getId() { return id; } public void setId(Long uid) { this.id = uid; } public Long getBalance() { return balance; } public void setBalance(Long balance) { this.balance = balance; } @Version public long getVersion() { return version; } public void setVersion(long version) { this.version = version; }
}
@Transactional public void transfer4(Long from, Long to, Long amount) { Money fromAcount = template .load(Money.class, from, LockMode.OPTIMISTIC); Money toAccount = template.load(Money.class, to, LockMode.OPTIMISTIC);fromAcount.setBalance(fromAcount.getBalance() - amount); // //////////////// try { Thread.sleep(1000l); } catch (InterruptedException e) { e.printStackTrace(); } // //////////////// toAccount.setBalance(toAccount.getBalance() + amount); template.save(fromAcount); template.save(toAccount);
}
换成乐观锁后,一个线程的事务顺利执行成功, 而另一个线程则抛出了乐观锁异常回滚掉了。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于