一、前言
1. 事务4个特性
1.原子性(atomic),事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行
2.一致性(consistent),事务在完成时,必须使所有的数据都保持一致状态。
3.隔离性(insulation),由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。
4.持久性(Duration),事务完成之后,它对于系统的影响是永久性的。
2. 事务并发
通常为了获得更好的运行性能,各种数据库都允许多个事务同时运行,这就是事务并发。3. 事务隔离机制
当并发的事务访问或修改数据库中相同的数据时,通常需要采取必要的隔离机制。
解决并发问题的途径是什么?答案是:采取有效的隔离机制。
怎样实现事务的隔离呢?隔离机制的实现必须使用锁
4. 事务并发问题
1.第一类丢失更新:(在秒杀场景会出现问题)
当事务A和事务B同时修改某行的值,
1.事务A将数值改为1并提交
2.事务B将数值改为2并提交。这时数据的值为2,事务A所做的更新将会丢失。
解决办法:对行加锁,只允许并发一个更新事务。(hibernate中的悲观锁,乐观锁)
2.脏读
1.李四的原工资为8000, 财务人员将李四的工资改为了10000(但未提交事务)
2.李四读取自己的工资 ,发现自己的工资变为了10000,欢天喜地!(在缓存中读取)
3.而财务发现操作有误,回滚了事务,张三的工资又变为了8000 像这样,张三记取的工资数10000是一个脏数据。
解决办法:如果在第一个事务提交前,任何其他事务不可读取其修改过的值,则可以避免该问题。
3.虚读
目前工资为5000的员工有20人。
1.事务1,读取所有工资为5000的员工。
2.这时事务2向employee表插入了一条员工记录,工资也为5000
3.事务1再次读取所有工资为5000的员工共读取到了11条记录,
解决办法:如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可避免该问题。
4.不可重复读
在一个事务中前后两次读取的结果并不致,导致了不可重复读。
1.在事务1中,Mary 读取了自己的工资为1000,操作并没有完成
2.在事务2中,这时财务人员修改了Mary的工资为2000,并提交了事务.
3.在事务1中,Mary 再次读取自己的工资时,工资变为了2000
解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。
5.第二类丢失更新
多个事务同时读取相同数据,并完成各自的事务提交,导致最后一个事务提交会覆盖前面所有事务对数据的改变
5. 悲观锁乐观锁介绍
1. 悲观锁
如果使用了悲观锁(加了一个行锁),如果事务没有被释放,就会造成其他事务处于等待,所以这里不使用悲观锁。
使用数据库提供的锁机制实现悲观锁。
如果数据库不支持设置的锁机制,hibernate会使用该数据库提供的合适的锁机制来完成,而不会报错。
使用session.load(class,id,LockOptions);加悲观锁,相当于发送SELECT ... FOR UPDATE
使用session.get(class,id,LockOptions);加悲观锁,相当于发送SELECT ... FOR UPDATE
使用session.buildLockRequest(LockOptions).lock(entity);加悲观锁,相当于发送SELECT id
FROM ... FOR UPDATE
使用query.setLockOptions(LockOptions);加悲观锁,相当于发送SELECT... FRO UPDATE
2.乐观锁
推荐使用 version方式;
version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。
当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功
二、代码测试
1. 控制库存不能为负
//控制不能卖出负数的产品mysql不支持check约束,不能通过数据库控制控制负数 public void setNum(Integer num) { if (num < 0) { throw new RuntimeException("库存不够,请刷新再购买"); } t his.num = num; }
2.没有加乐观锁
1.多事务顺序执行。假设总库存10件
//事务1初始库存10件,卖出去5件,最终库存5件 //事务2卖出8件最终库存‐3件,库存为负数。 @Test public void update() throws Exception { // 事务1 Session session = HibernateUtils.getSession(); session.beginTransaction(); Product product = (Product) session.get(Product.class, 1L); product.setNumber(product.getNumber() ‐ 5); session.update(product); session.getTransaction().commit(); session.close(); // 事务2 Session session2 = HibernateUtils.getSession(); session2.beginTransaction(); Product product2 = (Product) session2.get(Product.class, 1L); product2.setNumber(product2.getNumber() ‐ 8); session2.update(product2); session2.getTransaction().commit(); session2.close(); }
2.多事务交叉执行。
//事务1初始库存10件,卖出去8件,最终库存2件,还没有提交。 //事务2卖出5件最终库存5件,库存为5提交事务后库存为5。 //卖出10件库存卖出13件,剩余库存5件。 @Test public void update2() throws Exception { Session session = HibernateUtils.getSession(); Session session2 = HibernateUtils.getSession(); session.beginTransaction(); session2.beginTransaction(); Product product2 = (Product) session2.get(Product.class, 1L); Product product = (Product) session.get(Product.class, 1L);// 库存都是10 product2.setNumber(product2.getNumber() ‐ 8);// 10‐8=2 product.setNumber(product.getNumber() ‐ 5);// 10‐5=5 session.update(product); session2.update(product2); session2.getTransaction().commit();// 更新为2 session.getTransaction().commit();// 更新为5 session2.close(); session.close(); }
3.从上面看出,没有加乐观锁。无论事务顺序执行还是交叉执行,库存都会出问题。
3. 加了乐观锁
准备工作
//添加一个字段Integer version,不由程序员维护,由hibernate自己维护 private Integer version; 映射文件
实体类代码
public class Product { private Long id; private String name; private Integer num; // 添加一个字段Integer version,不由程序员维护,由hibernate自己维护 private Integer version; public Long getId() { return id; } p ublic void setId(Long id) { this.id = id; } p ublic String getName() { return name; } p ublic void setName(String name) { this.name = name; } p ublic Integer getNum() { return num; } p ublic void setNum(Integer num) { if (num < 0) { throw new RuntimeException("库存不够,请刷新再购买"); } t his.num = num; } @ Override public String toString() { return "Product [id=" + id + ", name=" + name + ", num=" + num + "]"; } }
映射文件代码
测试代码
//假设库存为1。 //事务操作流程:先查询,获取库存,在购买,再更新 //事务1查询select o from table where id=100 num=10 version=0 //事务2查询select o from table where id=100 num=10 version=0 //事务1查询购买8件 //事务2查询购买5件 //事务2更新,提交 库存5件 //Update xxx set version=version+1 num=? //Where id=100 and version=0 //事务1更新,报错,抛出异常 //Update xxx set version=version+1 num=? //Where id=100 and version=0 因为version已经改变 //初始库存10件,卖出去5件,最终库存5件 //如果出现乐观锁异常,就捕获StaleObjectStateException异常 //功能实现。 @Test public void update3() throws Exception { try { Session session = HibernateUtils.getSession(); Session session2 = HibernateUtils.getSession(); session.beginTransaction(); session2.beginTransaction(); Product product2 = (Product) session2.get(Product.class, 1L);// 库存都是1 version=0 Product product = (Product) session.get(Product.class, 1L);// 库存都是1 version=0 product2.setNumber(product2.getNumber() ‐ 1);// 1‐1=0 product.setNumber(product.getNumber() ‐ 1);// 1‐1=0 session.update(product); session2.update(product2); // update Product set version=1, name=?, price=?, number=0 where id=1 and version=0 // 数据库最新是version=1 session2.getTransaction().commit();// 更新为2 // update Product set version=1, name=?, price=?, number=0 where id=1 and version=0 // version=0 数据库最新是version=1 session.getTransaction().commit();// 异常 session2.close(); session.close(); } catch (StaleObjectStateException e) {// 库存已经更新 Session session3 = HibernateUtils.getSession(); Product product3 = (Product) session3.get(Product.class, 1L); System.out.println("库存已经更新,最新库存为:" + product3.getNumber()); session3.close(); }
4. 总结:
以上是hibernate框架使用乐观锁的version方式处理库存不为负的代码测试。谢谢观看。
閱讀更多 黑馬程序員成都中心 的文章