A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

本帖最后由 小蜀哥哥 于 2018-11-15 15:17 编辑

一、前言  

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. 控制库存不能为负
[Java] 纯文本查看 复制代码
//控制不能卖出负数的产品mysql不支持check约束,不能通过数据库控制控制负数
public void setNum(Integer num) {
if (num < 0) {
throw new RuntimeException("库存不够,请刷新再购买");
} t
his.num = num;
}


2.没有加乐观锁
1.多事务顺序执行。假设总库存10件  
[Java] 纯文本查看 复制代码
//事务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.多事务交叉执行。
[Java] 纯文本查看 复制代码
//事务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. 加了乐观锁
准备工作
[Java] 纯文本查看 复制代码
//添加一个字段Integer version,不由程序员维护,由hibernate自己维护
private Integer version;
映射文件
<!‐‐ 必须在id元素之后进行配置 ‐‐>
<!‐‐ access=默认值就是property(属性),如果需要访问字段field(字段) ‐‐>
<version name="version" access="field" />

实体类代码
[Java] 纯文本查看 复制代码
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 + "]";
}
}

映射文件代码  

[XML] 纯文本查看 复制代码
<hibernate‐mapping package="cn.itheima.hibernate.lock">
<class name="Product">
<id name="id">
<generator class="native" />
</id>
<!‐‐ 必须在id元素之后进行配置 ‐‐>
<!‐‐ access=默认值就是property(属性),如果需要访问字段field(字段) ‐‐>
<version name="version" access="field" />
<property name="name" />
<property name="num" />
</class>
</hibernate‐mapping>

测试代码
[Java] 纯文本查看 复制代码
//假设库存为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方式处理库存不为负的代码测试。谢谢观看。
  
  
  
  











1 个回复

倒序浏览
小蜀哥哥果然给力
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马