hibernate和mybatis中数据库锁的应用

一、为什么要使用锁?

要想弄清楚锁机制存在的原因,首先要了解事务的概念。
事务是对数据库一系列相关的操作,它必须具备ACID特征:

A(原子性):要么全部成功,要么全部撤销
C(一致性):要保持数据库的一致性
I(隔离性):不同事务操作相同数据时,要有各自的数据空间
D(持久性):一旦事务成功结束,它对数据库所做的更新必须永久保持

我们常用的关系型数据库RDBMS实现了事务的这些特性。其中,原子性、
一致性和持久性都是采用日志来保证的。而隔离性就是由今天我们关注的
锁机制来实现的,这就是为什么我们需要锁机制

如果没有锁,对隔离性不加控制,可能会造成哪些后果呢?

1.更新丢失:事务1提交的数据被事务2覆盖。
2.脏读:事务2查询到了事务1未提交的数据。
3.虚读:事务2查询到了事务1提交的新建数据。
4.不可重复读:事务2查询到了事务1提交的更新数据。

Hibernate中锁的应用:

下面来看Hibernate的例子,两个线程分别开启两个事务操作tb_account表中
的同一行数据col_id=1。
[java]  view plain copy
  1. package com.cdai.orm.hibernate.annotation;  
  2.   
  3. import java.io.Serializable;  
  4.   
  5. import javax.persistence.Column;  
  6. import javax.persistence.Entity;  
  7. import javax.persistence.Id;  
  8. import javax.persistence.Table;  
  9.   
  10. @Entity  
  11. @Table(name = "tb_account")  
  12. public class Account implements Serializable {  
  13.   
  14.     private static final long serialVersionUID = 5018821760412231859L;  
  15.   
  16.     @Id  
  17.     @Column(name = "col_id")  
  18.     private long id;  
  19.       
  20.     @Column(name = "col_balance")  
  21.     private long balance;  
  22.   
  23.     public Account() {  
  24.     }  
  25.       
  26.     public Account(long id, long balance) {  
  27.         this.id = id;  
  28.         this.balance = balance;  
  29.     }  
  30.   
  31.     public long getId() {  
  32.         return id;  
  33.     }  
  34.   
  35.     public void setId(long id) {  
  36.         this.id = id;  
  37.     }  
  38.   
  39.     public long getBalance() {  
  40.         return balance;  
  41.     }  
  42.   
  43.     public void setBalance(long balance) {  
  44.         this.balance = balance;  
  45.     }  
  46.   
  47.     @Override  
  48.     public String toString() {  
  49.         return "Account [id=" + id + ", balance=" + balance + "]";  
  50.     }  
  51.       
  52. }  

[java]  view plain copy
  1. package com.cdai.orm.hibernate.transaction;  
  2.   
  3. import org.hibernate.Session;  
  4. import org.hibernate.SessionFactory;  
  5. import org.hibernate.Transaction;  
  6. import org.hibernate.cfg.AnnotationConfiguration;  
  7.   
  8. import com.cdai.orm.hibernate.annotation.Account;  
  9.   
  10. public class DirtyRead {  
  11.   
  12.     public static void main(String[] args) {  
  13.   
  14.         final SessionFactory sessionFactory = new AnnotationConfiguration().  
  15.                 addFile("hibernate/hibernate.cfg.xml").               
  16.                 configure().  
  17.                 addPackage("com.cdai.orm.hibernate.annotation").  
  18.                 addAnnotatedClass(Account.class).  
  19.                 buildSessionFactory();  
  20.           
  21.         Thread t1 = new Thread() {  
  22.               
  23.             @Override  
  24.             public void run() {  
  25.                 Session session1 = sessionFactory.openSession();  
  26.                 Transaction tx1 = null;  
  27.                 try {  
  28.                     tx1 = session1.beginTransaction();  
  29.                     System.out.println("T1 - Begin trasaction");  
  30.                     Thread.sleep(500);  
  31.                       
  32.                     Account account = (Account)   
  33.                             session1.get(Account.classnew Long(1));  
  34.                     System.out.println("T1 - balance=" + account.getBalance());  
  35.                     Thread.sleep(500);  
  36.                       
  37.                     account.setBalance(account.getBalance() + 100);  
  38.                     System.out.println("T1 - Change balance:" + account.getBalance());  
  39.                       
  40.                     tx1.commit();  
  41.                     System.out.println("T1 - Commit transaction");  
  42.                     Thread.sleep(500);  
  43.                 }  
  44.                 catch (Exception e) {  
  45.                     e.printStackTrace();  
  46.                     if (tx1 != null)  
  47.                         tx1.rollback();  
  48.                 }   
  49.                 finally {  
  50.                     session1.close();  
  51.                 }  
  52.             }  
  53.               
  54.         };  
  55.           
  56.         // 3.Run transaction 2  
  57.         Thread t2 = new Thread() {  
  58.               
  59.             @Override  
  60.             public void run() {  
  61.                 Session session2 = sessionFactory.openSession();  
  62.                 Transaction tx2 = null;  
  63.                 try {  
  64.                     tx2 = session2.beginTransaction();  
  65.                     System.out.println("T2 - Begin trasaction");  
  66.                     Thread.sleep(500);  
  67.                       
  68.                     Account account = (Account)   
  69.                             session2.get(Account.classnew Long(1));  
  70.                     System.out.println("T2 - balance=" + account.getBalance());  
  71.                     Thread.sleep(500);  
  72.                       
  73.                     account.setBalance(account.getBalance() - 100);  
  74.                     System.out.println("T2 - Change balance:" + account.getBalance());  
  75.                       
  76.                     tx2.commit();  
  77.                     System.out.println("T2 - Commit transaction");  
  78.                     Thread.sleep(500);  
  79.                 }  
  80.                 catch (Exception e) {  
  81.                     e.printStackTrace();  
  82.                     if (tx2 != null)  
  83.                         tx2.rollback();  
  84.                 }   
  85.                 finally {  
  86.                     session2.close();  
  87.                 }  
  88.             }  
  89.               
  90.         };  
  91.           
  92.         t1.start();  
  93.         t2.start();  
  94.           
  95.         while (t1.isAlive() || t2.isAlive()) {  
  96.             try {  
  97.                 Thread.sleep(2000L);  
  98.             } catch (InterruptedException e) {  
  99.             }  
  100.         }  
  101.           
  102.         System.out.println("Both T1 and T2 are dead.");  
  103.         sessionFactory.close();  
  104.           
  105.     }  
  106.   
  107. }  
事务1将col_balance减小100,而事务2将其减少100,最终结果可能是0,也
可能是200,事务1或2的更新可能会丢失。log输出也印证了这一点,事务1和2
的log交叉打印。

T1 - Begin trasaction
T2 - Begin trasaction
Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ where account0_.col_id=?
Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ where account0_.col_id=?
T1 - balance=100
T2 - balance=100
T2 - Change balance:0
T1 - Change balance:200
Hibernate: update tb_account set col_balance=? where col_id=?
Hibernate: update tb_account set col_balance=? where col_id=?
T1 - Commit transaction
T2 - Commit transaction
Both T1 and T2 are dead.



由此可见,隔离性是一个需要慎重考虑的问题,理解锁很有必要。

有多少种锁?

常见的有共享锁、更新锁和独占锁。
1.共享锁:用于读数据操作,允许其他事务同时读取。当事务执行select语句时, 数据库自动为事务分配一把共享锁来锁定读取的数据。
2.独占锁:用于修改数据,其他事务不能读取也不能修改。当事务执行insert、 update和delete时,数据库会自动分配。
3.更新锁:用于避免更新操作时共享锁造成的死锁,比如事务1和2同时持有 共享锁并等待获得独占锁。当执行update时,事务先获得更新锁,然后将
更新锁升级成独占锁,这样就避免了死锁。

此外,这些锁都可以施加到数据库中不同的对象上,即这些锁可以有不同的粒度。
如数据库级锁、表级锁、页面级锁、键级锁和行级锁。

所以锁是有很多种的,这么多锁要想完全掌握灵活使用太难了,我们又不是DBA。
怎么办?还好,锁机制对于我们一般用户来说是透明的,数据库会自动添加合适的
锁,并在适当的时机自动升级、降级各种锁,真是太周到了!我们只需要做的就是
学会根据不同的业务需求,设置好隔离级别就可以了。

怎样设置隔离级别?

一般来说,数据库系统会提供 四种事务隔离级别 供用户选择:

1.Serializable(串行化):当两个事务同时操纵相同数据时,事务2只能停下来等。

2.Repeatable Read(可重复读):事务1能看到事务2新插入的数据,不能看到对
已有数据的更新。

3.Read Commited(读已提交数据:事务1能看到事务2新插入和更新的数据。

4.Read Uncommited(读未提交数据):事务1能看到事务2没有提交的插入和更新
数据。

应用程序中的锁


当数据库采用Read Commited隔离级别时,可以在应用程序中采用悲观锁或乐观锁

1.悲观锁 :假定当前事务操作的数据肯定还会有其他事务访问,因此悲观地在应用
程序中显式指定采用独占锁来锁定数据资源。在MySQL、Oracle中支持以下形式:

     select ... for update

显式地让select采用独占锁锁定查询的记录,其他事务要查询、更新或删除这些被
锁定的数据,都要等到该事务结束后才行。

Hibernate中,可以在load时传入LockMode.UPGRADE来采用悲观锁。修改前面的例子,
在事务1和2的get方法调用处,多传入一个LockMode参数。从log中可以看出,事务1和2
不再是交叉运行,事务2等待事务1结束后才可以读取数据,所以最终col_balance值是正确
的100。
[java]  view plain copy
  1. package com.cdai.orm.hibernate.transaction;  
  2.   
  3. import org.hibernate.LockMode;  
  4. import org.hibernate.Session;  
  5. import org.hibernate.SessionFactory;  
  6. import org.hibernate.Transaction;  
  7.   
  8. import com.cdai.orm.hibernate.annotation.Account;  
  9. import com.cdai.orm.hibernate.annotation.AnnotationHibernate;  
  10.   
  11. public class UpgradeLock {  
  12.   
  13.     @SuppressWarnings("deprecation")  
  14.     public static void main(String[] args) {  
  15.   
  16.         final SessionFactory sessionFactory = AnnotationHibernate.createSessionFactory();   
  17.   
  18.         // Run transaction 1  
  19.         Thread t1 = new Thread() {  
  20.               
  21.             @Override  
  22.             public void run() {  
  23.                 Session session1 = sessionFactory.openSession();  
  24.                 Transaction tx1 = null;  
  25.                 try {  
  26.                     tx1 = session1.beginTransaction();  
  27.                     System.out.println("T1 - Begin trasaction");  
  28.                     Thread.sleep(500);  
  29.                       
  30.                     Account account = (Account)   
  31.                             session1.get(Account.classnew Long(1), LockMode.UPGRADE);  
  32.                     System.out.println("T1 - balance=" + account.getBalance());  
  33.                     Thread.sleep(500);  
  34.                       
  35.                     account.setBalance(account.getBalance() + 100);  
  36.                     System.out.println("T1 - Change balance:" + account.getBalance());  
  37.                       
  38.                     tx1.commit();  
  39.                     System.out.println("T1 - Commit transaction");  
  40.                     Thread.sleep(500);  
  41.                 }  
  42.                 catch (Exception e) {  
  43.                     e.printStackTrace();  
  44.                     if (tx1 != null)  
  45.                         tx1.rollback();  
  46.                 }   
  47.                 finally {  
  48.                     session1.close();  
  49.                 }  
  50.             }  
  51.               
  52.         };  
  53.           
  54.         // Run transaction 2  
  55.         Thread t2 = new Thread() {  
  56.               
  57.             @Override  
  58.             public void run() {  
  59.                 Session session2 = sessionFactory.openSession();  
  60.                 Transaction tx2 = null;  
  61.                 try {  
  62.                     tx2 = session2.beginTransaction();  
  63.                     System.out.println("T2 - Begin trasaction");  
  64.                     Thread.sleep(500);  
  65.                       
  66.                     Account account = (Account)   
  67.                             session2.get(Account.classnew Long(1), LockMode.UPGRADE);  
  68.                     System.out.println("T2 - balance=" + account.getBalance());  
  69.                     Thread.sleep(500);  
  70.                       
  71.                     account.setBalance(account.getBalance() - 100);  
  72.                     System.out.println("T2 - Change balance:" + account.getBalance());  
  73.                       
  74.                     tx2.commit();  
  75.                     System.out.println("T2 - Commit transaction");  
  76.                     Thread.sleep(500);  
  77.                 }  
  78.                 catch (Exception e) {  
  79.                     e.printStackTrace();  
  80.                     if (tx2 != null)  
  81.                         tx2.rollback();  
  82.                 }   
  83.                 finally {  
  84.                     session2.close();  
  85.                 }  
  86.             }  
  87.               
  88.         };  
  89.           
  90.         t1.start();  
  91.         t2.start();  
  92.           
  93.         while (t1.isAlive() || t2.isAlive()) {  
  94.             try {  
  95.                 Thread.sleep(2000L);  
  96.             } catch (InterruptedException e) {  
  97.             }  
  98.         }  
  99.           
  100.         System.out.println("Both T1 and T2 are dead.");  
  101.         sessionFactory.close();  
  102.   
  103.     }  
  104.   
  105. }  
T1 - Begin trasaction
T2 - Begin trasaction
Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ with (updlock, rowlock) where account0_.col_id=?
Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ with (updlock, rowlock) where account0_.col_id=?
T2 - balance=100
T2 - Change balance:0
Hibernate: update tb_account set col_balance=? where col_id=?
T2 - Commit transaction
T1 - balance=0
T1 - Change balance:100
Hibernate: update tb_account set col_balance=? where col_id=?
T1 - Commit transaction
Both T1 and T2 are dead.

Hibernate对于SQLServer 2005会执行SQL:

select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ with (updlock, rowlock) where account0_.col_id=?

为选定的col_id为1的数据行加上行锁和更新锁。

2.乐观锁:假定当前事务操作的数据不会有其他事务同时访问,因此完全依靠数据库
的隔离级别来自动管理锁的工作。在应用程序中采用版本控制来避免可能低概率出现
的并发问题。

Hibernate中,使用Version注解来定义版本号字段




将DirtyLock中的Account对象替换成AccountVersion,其他代码不变,执行出现异常。
[java]  view plain copy
  1. package com.cdai.orm.hibernate.transaction;  
  2.   
  3. import javax.persistence.Column;  
  4. import javax.persistence.Entity;  
  5. import javax.persistence.Id;  
  6. import javax.persistence.Table;  
  7. import javax.persistence.Version;  
  8.   
  9. @Entity  
  10. @Table(name = "tb_account_version")  
  11. public class AccountVersion {  
  12.   
  13.     @Id  
  14.     @Column(name = "col_id")  
  15.     private long id;  
  16.       
  17.     @Column(name = "col_balance")  
  18.     private long balance;  
  19.       
  20.     @Version  
  21.     @Column(name = "col_version")  
  22.     private int version;  
  23.   
  24.     public AccountVersion() {  
  25.     }  
  26.   
  27.     public AccountVersion(long id, long balance) {  
  28.         this.id = id;  
  29.         this.balance = balance;  
  30.     }  
  31.   
  32.     public long getId() {  
  33.         return id;  
  34.     }  
  35.   
  36.     public void setId(long id) {  
  37.         this.id = id;  
  38.     }  
  39.   
  40.     public long getBalance() {  
  41.         return balance;  
  42.     }  
  43.   
  44.     public void setBalance(long balance) {  
  45.         this.balance = balance;  
  46.     }  
  47.   
  48.     public int getVersion() {  
  49.         return version;  
  50.     }  
  51.   
  52.     public void setVersion(int version) {  
  53.         this.version = version;  
  54.     }  
  55.       
  56. }  
log如下:

T1 - Begin trasaction
T2 - Begin trasaction
Hibernate: select accountver0_.col_id as col1_0_0_, accountver0_.col_balance as col2_0_0_, accountver0_.col_version as col3_0_0_ from tb_account_version accountver0_ where accountver0_.col_id=?
Hibernate: select accountver0_.col_id as col1_0_0_, accountver0_.col_balance as col2_0_0_, accountver0_.col_version as col3_0_0_ from tb_account_version accountver0_ where accountver0_.col_id=?
T1 - balance=1000
T2 - balance=1000
T1 - Change balance:900
T2 - Change balance:1100
Hibernate: update tb_account_version set col_balance=?, col_version=? where col_id=? and col_version=?
Hibernate: update tb_account_version set col_balance=?, col_version=? where col_id=? and col_version=?
T1 - Commit transaction
2264 [Thread-2] ERROR org.hibernate.event.def.AbstractFlushingEventListener - Could not synchronize database state with session
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.cdai.orm.hibernate.transaction.AccountVersion#1]
     at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1934)
     at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2578)
     at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2478)
     at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2805)
     at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:114)
     at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:268)
     at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:260)
     at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:180)
     at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
     at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
     at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1206)
     at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:375)
     at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:137)
     at com.cdai.orm.hibernate.transaction.VersionLock$2.run(VersionLock.java:93)
Both T1 and T2 are dead.

由于乐观锁完全将事务隔离交给数据库来控制,所以事务1和2交叉运行了,事务1提交
成功并将col_version改为1,然而事务2提交时已经找不到col_version为0的数据了,所以
抛出了异常。



 

mybatis中锁的应用:

  根据目前的了解,mybatis中没有入hibernate中的@version标签类似的功能,因此:mybatis要实现悲观锁和乐观锁的功能,只能通过程序实现。

悲观锁实现方式:

悲观锁就是数据库里面锁住 类似for update查询 ,在select  语句后面加上 for update 的方式。

乐观锁实现方式:

在需要锁定的数据行中增加一个version的字段(当然也可以用其他名称),在update语句中,必须实现如下格式:

UPDATE 必须这样写:

UPDATE T_USER u
   SET u.address = #address#,
       u.version = u.version + 1
 WHERE u.username = #username#
   AND u.version = #version#


你可能感兴趣的:(mybatis,Hibernate,mybatis)