zoukankan      html  css  js  c++  java
  • Hibernate与数据库锁


    一、为什么要使用锁?

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

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

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

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

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

    下面来看Hibernate的例子,两个线程分别开启两个事务操作tb_account表中
    的同一行数据col_id=1。
    package com.cdai.orm.hibernate.annotation;
    
    import java.io.Serializable;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.Id;
    import javax.persistence.Table;
    
    @Entity
    @Table(name = "tb_account")
    public class Account implements Serializable {
    
    	private static final long serialVersionUID = 5018821760412231859L;
    
    	@Id
    	@Column(name = "col_id")
    	private long id;
    	
    	@Column(name = "col_balance")
    	private long balance;
    
    	public Account() {
    	}
    	
    	public Account(long id, long balance) {
    		this.id = id;
    		this.balance = balance;
    	}
    
    	public long getId() {
    		return id;
    	}
    
    	public void setId(long id) {
    		this.id = id;
    	}
    
    	public long getBalance() {
    		return balance;
    	}
    
    	public void setBalance(long balance) {
    		this.balance = balance;
    	}
    
    	@Override
    	public String toString() {
    		return "Account [id=" + id + ", balance=" + balance + "]";
    	}
    	
    }

    package com.cdai.orm.hibernate.transaction;
    
    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.hibernate.Transaction;
    import org.hibernate.cfg.AnnotationConfiguration;
    
    import com.cdai.orm.hibernate.annotation.Account;
    
    public class DirtyRead {
    
    	public static void main(String[] args) {
    
    		final SessionFactory sessionFactory = new AnnotationConfiguration().
    				addFile("hibernate/hibernate.cfg.xml").				
    				configure().
    				addPackage("com.cdai.orm.hibernate.annotation").
    				addAnnotatedClass(Account.class).
    				buildSessionFactory();
    		
    		Thread t1 = new Thread() {
    			
    			@Override
    			public void run() {
    				Session session1 = sessionFactory.openSession();
    				Transaction tx1 = null;
    				try {
    					tx1 = session1.beginTransaction();
    					System.out.println("T1 - Begin trasaction");
    					Thread.sleep(500);
    					
    					Account account = (Account) 
    							session1.get(Account.class, new Long(1));
    					System.out.println("T1 - balance=" + account.getBalance());
    					Thread.sleep(500);
    					
    					account.setBalance(account.getBalance() + 100);
    					System.out.println("T1 - Change balance:" + account.getBalance());
    					
    					tx1.commit();
    					System.out.println("T1 - Commit transaction");
    					Thread.sleep(500);
    				}
    				catch (Exception e) {
    					e.printStackTrace();
    					if (tx1 != null)
    						tx1.rollback();
    				} 
    				finally {
    					session1.close();
    				}
    			}
    			
    		};
    		
    		// 3.Run transaction 2
    		Thread t2 = new Thread() {
    			
    			@Override
    			public void run() {
    				Session session2 = sessionFactory.openSession();
    				Transaction tx2 = null;
    				try {
    					tx2 = session2.beginTransaction();
    					System.out.println("T2 - Begin trasaction");
    					Thread.sleep(500);
    					
    					Account account = (Account) 
    							session2.get(Account.class, new Long(1));
    					System.out.println("T2 - balance=" + account.getBalance());
    					Thread.sleep(500);
    					
    					account.setBalance(account.getBalance() - 100);
    					System.out.println("T2 - Change balance:" + account.getBalance());
    					
    					tx2.commit();
    					System.out.println("T2 - Commit transaction");
    					Thread.sleep(500);
    				}
    				catch (Exception e) {
    					e.printStackTrace();
    					if (tx2 != null)
    						tx2.rollback();
    				} 
    				finally {
    					session2.close();
    				}
    			}
    			
    		};
    		
    		t1.start();
    		t2.start();
    		
    		while (t1.isAlive() || t2.isAlive()) {
    			try {
    				Thread.sleep(2000L);
    			} catch (InterruptedException e) {
    			}
    		}
    		
    		System.out.println("Both T1 and T2 are dead.");
    		sessionFactory.close();
    		
    	}
    
    }
    事务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。
    package com.cdai.orm.hibernate.transaction;
    
    import org.hibernate.LockMode;
    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.hibernate.Transaction;
    
    import com.cdai.orm.hibernate.annotation.Account;
    import com.cdai.orm.hibernate.annotation.AnnotationHibernate;
    
    public class UpgradeLock {
    
    	@SuppressWarnings("deprecation")
    	public static void main(String[] args) {
    
    		final SessionFactory sessionFactory = AnnotationHibernate.createSessionFactory(); 
    
    		// Run transaction 1
    		Thread t1 = new Thread() {
    			
    			@Override
    			public void run() {
    				Session session1 = sessionFactory.openSession();
    				Transaction tx1 = null;
    				try {
    					tx1 = session1.beginTransaction();
    					System.out.println("T1 - Begin trasaction");
    					Thread.sleep(500);
    					
    					Account account = (Account) 
    							session1.get(Account.class, new Long(1), LockMode.UPGRADE);
    					System.out.println("T1 - balance=" + account.getBalance());
    					Thread.sleep(500);
    					
    					account.setBalance(account.getBalance() + 100);
    					System.out.println("T1 - Change balance:" + account.getBalance());
    					
    					tx1.commit();
    					System.out.println("T1 - Commit transaction");
    					Thread.sleep(500);
    				}
    				catch (Exception e) {
    					e.printStackTrace();
    					if (tx1 != null)
    						tx1.rollback();
    				} 
    				finally {
    					session1.close();
    				}
    			}
    			
    		};
    		
    		// Run transaction 2
    		Thread t2 = new Thread() {
    			
    			@Override
    			public void run() {
    				Session session2 = sessionFactory.openSession();
    				Transaction tx2 = null;
    				try {
    					tx2 = session2.beginTransaction();
    					System.out.println("T2 - Begin trasaction");
    					Thread.sleep(500);
    					
    					Account account = (Account) 
    							session2.get(Account.class, new Long(1), LockMode.UPGRADE);
    					System.out.println("T2 - balance=" + account.getBalance());
    					Thread.sleep(500);
    					
    					account.setBalance(account.getBalance() - 100);
    					System.out.println("T2 - Change balance:" + account.getBalance());
    					
    					tx2.commit();
    					System.out.println("T2 - Commit transaction");
    					Thread.sleep(500);
    				}
    				catch (Exception e) {
    					e.printStackTrace();
    					if (tx2 != null)
    						tx2.rollback();
    				} 
    				finally {
    					session2.close();
    				}
    			}
    			
    		};
    		
    		t1.start();
    		t2.start();
    		
    		while (t1.isAlive() || t2.isAlive()) {
    			try {
    				Thread.sleep(2000L);
    			} catch (InterruptedException e) {
    			}
    		}
    		
    		System.out.println("Both T1 and T2 are dead.");
    		sessionFactory.close();
    
    	}
    
    }
    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,其他代码不变,执行出现异常。
    package com.cdai.orm.hibernate.transaction;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.Id;
    import javax.persistence.Table;
    import javax.persistence.Version;
    
    @Entity
    @Table(name = "tb_account_version")
    public class AccountVersion {
    
    	@Id
    	@Column(name = "col_id")
    	private long id;
    	
    	@Column(name = "col_balance")
    	private long balance;
    	
    	@Version
    	@Column(name = "col_version")
    	private int version;
    
    	public AccountVersion() {
    	}
    
    	public AccountVersion(long id, long balance) {
    		this.id = id;
    		this.balance = balance;
    	}
    
    	public long getId() {
    		return id;
    	}
    
    	public void setId(long id) {
    		this.id = id;
    	}
    
    	public long getBalance() {
    		return balance;
    	}
    
    	public void setBalance(long balance) {
    		this.balance = balance;
    	}
    
    	public int getVersion() {
    		return version;
    	}
    
    	public void setVersion(int version) {
    		this.version = version;
    	}
    	
    }
    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的数据了,所以
    抛出了异常。




  • 相关阅读:
    stack.pop()和stack.peek()的区别
    信号与系统,系统函数的影响
    java中short、int、long、float、double取值范围
    Spring从容器获得组件的方法
    Eclipse中项目的类路径文件夹
    Math的常用方法
    spring基本入门步骤
    opencv入门
    make和cmake构建工具
    使用eclipse开发c++程序及开发环境搭建
  • 原文地址:https://www.cnblogs.com/xiaomaohai/p/6157821.html
Copyright © 2011-2022 走看看