悲观锁和乐观锁
一、背景介绍
悲观锁和乐观锁的出现是为了解决并发编程中的竞态条件和数据一致性问题。
本篇博客主要介绍悲观锁和乐观锁的基本概念,以及如何使用悲观锁和乐观锁应用到项目中,以及悲观锁和乐观锁的使用场景。
二、悲观锁和乐观锁
什么是悲观锁
悲观锁(Pessimistic Locking):对于并发控制策略采取悲观策略,认为并发访问会导致冲突。为了避免冲突悲观锁会在访问数据之前先对其进行加锁。确保同一时间只有一个线程能够对数据进行访问。
一般的悲观锁实现方式是使用数据库中的行级锁,和表级锁。当然jdk提供的synchronized 也是一种悲观锁。
什么是乐观锁
乐观锁(Optimistic Locking),对于并发控制策略采取乐观策略。它认为在数据被修改期间不会有其他线程对其进行修改。乐观锁在读取数据时不会进行加锁,而在更新数据时检查是否有其他线程对数据进行了修改。
一般乐观锁会使用版本号或者时间戳,来表示数据的版本,以便在更新的时候进行校验。
如果在校验时发现了数据已经被修改,则表示发送了冲突,需要进行相应的处理,例如重试、或者回滚。
乐观锁适用于读取操作频繁、冲突较少的场景,因为它避免了不必要的加锁操作,提高了并发性能。然而,如果冲突频繁发生,乐观锁可能需要进行多次重试,可能会降低性能。
三、 在项目中如何使用悲观锁和乐观锁
在项目中使用悲观锁
环境:项目架构为SSH架构,通过使用Hibernate的特性来支持悲观锁。
实体结构
package com.wangwei.hibernate; public class Inventory { private String itemNo; private String itemName; private int quantity; public String getItemName() { return itemName; } public void setItemName(String itemName) { this.itemName = itemName; } public String getItemNo() { return itemNo; } public void setItemNo(String itemNo) { this.itemNo = itemNo; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } }
实体对象的xml配置文件
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.wangwei.hibernate.Inventory" table="t_inventory"> <id name="itemNo"> <generator class="assigned"/> </id> <property name="itemName"/> <property name="quantity"/> </class> </hibernate-mapping>
对应生成的表结构
往表中初始化数据
package com.wangwei.hibernate; import org.hibernate.Session; public class InitData { /** * @param args */ public static void main(String[] args) { Session session = null; try { session = HibernateUtils.getSession(); session.beginTransaction(); Inventory inv = new Inventory(); inv.setItemNo("1001"); inv.setItemName("三鹿奶粉"); inv.setQuantity(1000); session.save(inv); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } } }
运行之后的结果
模拟触发悲观锁的条件
这段代码Inventory inv = (Inventory)session.load(Inventory.class, “1001”, LockMode.UPGRADE);中的LockMode.UPGRADE是使用了Hibernate的悲观锁,对查询的这条数据添加了行级锁。
核心代码
package com.wangwei.hibernate; import org.hibernate.LockMode; import org.hibernate.Session; import junit.framework.TestCase; public class PessimisticLockingTest extends TestCase { public void testLoad1() { Session session = null; try { session = HibernateUtils.getSession(); session.beginTransaction(); Inventory inv = (Inventory)session.load(Inventory.class, "1001", LockMode.UPGRADE); System.out.println("opt1-->itemNo=" + inv.getItemNo()); System.out.println("opt1-->itemName=" + inv.getItemName()); System.out.println("opt1-->quantity=" + inv.getQuantity()); inv.setQuantity(inv.getQuantity() - 200); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } } public void testLoad2() { Session session = null; try { session = HibernateUtils.getSession(); session.beginTransaction(); Inventory inv = (Inventory)session.load(Inventory.class, "1001", LockMode.UPGRADE); System.out.println("opt2-->itemNo=" + inv.getItemNo()); System.out.println("opt2-->itemName=" + inv.getItemName()); System.out.println("opt2-->quantity=" + inv.getQuantity()); inv.setQuantity(inv.getQuantity() - 200); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } } }
现在我们对testLoad1方法中的session.getTransaction().commit();打上断点。并进行运行
可以从图中看到,在查询语句后面有for update语句这就是添加上了行级锁(在没有提交事务之前,其他人无法操作这条数据,只有等待事务提交之后,才能操作这条数据)并且我们已经将这条数据查询出来了。
这个时候我们再运行testLoad2方法,通过图片我们可以看到并没有往下面进行执行,处于等待状态。这是由于这条记录被锁住了并且有其他人正在使用。之后等其他人使用完之后才能对这条数据进行操作。
我们再执行testLoad1打上断点的语句。可以看到等锁释放之后才会执行testLoad2的方法
在项目中使用乐观锁
环境:项目架构为SSH架构,通过使用Hibernate的特性来支持乐观锁。当我们知道实现的思路有原理之后,也可以手动的实现乐观锁的策略。
实现思路:大多数的使用是采用数据版本的方式(version)实现,一般在数据库表中加入一个version字段
在读取数据的时候将version读取出来,在保存或者更新数据的时候判断version的值是否小于数据库中的
version值,如果小于不予更新,否则给予更新。
实体结构(添加了version)
package com.wangwei.hibernate; public class Inventory { private String itemNo; private String itemName; private int quantity; private int version; public String getItemName() { return itemName; } public void setItemName(String itemName) { this.itemName = itemName; } public String getItemNo() { return itemNo; } public void setItemNo(String itemNo) { this.itemNo = itemNo; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } }
实体对应的xml配置文件
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.wangwei.hibernate.Inventory" table="t_inventory" optimistic-lock="version"> <id name="itemNo"> <generator class="assigned"/> </id> <version name="version"/> <property name="itemName"/> <property name="quantity"/> </class> </hibernate-mapping>
数据库表结构
往表中初始化数据
package com.wangwei.hibernate; import org.hibernate.Session; public class InitData { /** * @param args */ public static void main(String[] args) { Session session = null; try { session = HibernateUtils.getSession(); session.beginTransaction(); Inventory inv = new Inventory(); inv.setItemNo("1001"); inv.setItemName("三鹿奶粉"); inv.setQuantity(1000); session.save(inv); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } } }
运行之后的结果
实现的核心代码
package com.wangwei.hibernate; import org.hibernate.LockMode; import org.hibernate.Session; import junit.framework.TestCase; public class OptimisticLockingTest extends TestCase { public void testLoad1() { Session session = null; try { session = HibernateUtils.getSession(); session.beginTransaction(); Inventory inv = (Inventory)session.load(Inventory.class, "1001"); System.out.println("opt1-->itemNo=" + inv.getItemNo()); System.out.println("opt1-->itemName=" + inv.getItemName()); System.out.println("opt1-->version=" + inv.getVersion()); System.out.println("opt1-->quantity=" + inv.getQuantity()); inv.setQuantity(inv.getQuantity() - 200); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } } public void testLoad2() { Session session = null; try { session = HibernateUtils.getSession(); session.beginTransaction(); Inventory inv = (Inventory)session.load(Inventory.class, "1001"); System.out.println("opt2-->itemNo=" + inv.getItemNo()); System.out.println("opt2-->itemName=" + inv.getItemName()); System.out.println("opt2-->version=" + inv.getVersion()); System.out.println("opt2-->quantity=" + inv.getQuantity()); inv.setQuantity(inv.getQuantity() - 200); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } } }
触发乐观锁条件
先将断点打到代码:
session.getTransaction().commit();
我们先执行testLoad1方法
此时代码运行到session.getTransaction().commit();此时可以看到version为0 并且quantity为1000,此时事务并没有进行提交。
现在我们再运行testLoad2方法,按照testLoad2方法的执行之后其中的version为1并且quantity为800.
此时我们再执行testLoad1的断点,可以看到出现了错误,这是因为在ypdate的语句中使用了version作为条件,但是此时的数据库表version已经变为了1,而testLoad1方法开始查出并使用的version为0.所以就会出现错误。
那么上面是使用Hibernate框架时的情况,那我当我们使用的是JDBC的时候,那么由于修改的这条记录不存在,那么返回的受影响的行数会为0.
我们可以根据这个条件返回给前端,展示给用户,如:您目前的评论已经被覆盖,请重新评论等等的提示语。
三、 什么情况下使用悲观锁什么情况下使用乐观锁
悲观锁的使用场景
1.并发冲突概率高:如果在某个场景下,对共享资源的并发冲突概率很高,即多个线程同时修改同一份数据的可能性较大,那么使用悲观锁可以降低冲突风险。
2.复杂的操作:如果对共享资源进行复杂的操作,涉及多个步骤或依赖其他资源,使用悲观锁可以确保在整个操作过程中数据的一致性。
乐观锁的使用场景
1.并发冲突概率低:如果在某个场景下,并发冲突的概率较低,即多个线程修改同一份数据的可能性很小,那么可以使用乐观锁来提高系统的吞吐量和并发性能。
2.读操作远多于写操作:如果在某个场景下,对共享资源的读操作远远多于写操作,那么使用乐观锁可以避免不必要的阻塞,提高系统的性能
四、总结
综上所述:
悲观锁适用于并发冲突概率较高、对数据一致性要求较高的场景,而乐观锁适用于并发冲突概率较低、对性能要求较高的场景。在具体应用中,需要根据实际情况评估并选择适合的锁机制,或者结合使用多种锁机制以达到更好的性能和数据一致性。