6-初始-悲观锁与乐观锁

学习内容来源于蚂蚁课堂,感谢蚂蚁课堂大佬的指导,本文仅供学习记录使用,如有不适,请联系作者。

当程序中可能出现并发的情况时,就需要保证在并发情况下数据的准确性,以此确保当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这种手段就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响
没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。

举例:如下两张表,order订单表,money金额表,其中订单表status字段表示订单状态0 未支付,1已支付
订单表:
在这里插入图片描述
金额表:
在这里插入图片描述
现有两个JDBC连接同时对orderId = 1的订单做支付操作:
因为我们只用一条数据模拟,所以接下来的查询我们都会默认只能查出来一条

# 1.查询订单
SELECT * FROM `order` WHERE `status` = 0;
# 2.变更订单状态
UPDATE `order` SET `status` = 1 WHERE orderId = 1;
# 3. 修改实收金额
UPDATE `money` SET money = money + 500 WHERE orderId = 1;

我们代码演示可能出现的问题:
新建maven工程导入依赖:

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.tedu</groupId>
	<artifactId>02_concurrent_lock</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.30</version>
		</dependency>
		<dependency>
			<groupId>commons-dbcp</groupId>
			<artifactId>commons-dbcp</artifactId>
			<version>1.4</version>
		</dependency>
		<dependency>
			<groupId>commons-pool</groupId>
			<artifactId>commons-pool</artifactId>
			<version>1.5.6</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

自己写个JDBC连接工具类:

import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import org.apache.commons.dbcp.BasicDataSource;

public class DBUtils {
	static String driverClassName;
	static String url;
	static String username;
	static String password;
	static int setInitialSize;
	static int setMaxActive;
	static BasicDataSource ds;
	static {
		ds=new BasicDataSource();
		Properties cfg = new Properties();
		InputStream inStream = DBUtils.class.getClassLoader().getResourceAsStream("db.properties");
		try {
			cfg.load(inStream);
			driverClassName=cfg.getProperty("jdbc.className");
			url=cfg.getProperty("jdbc.url");
			username=cfg.getProperty("jdbc.user");
			password=cfg.getProperty("jdbc.password");
			setInitialSize=Integer.parseInt(cfg.getProperty("setInitialSize"));
			setMaxActive=Integer.parseInt(cfg.getProperty("setMaxActive"));
			ds.setDriverClassName(driverClassName);
			ds.setUrl(url);
			ds.setUsername(username);
			ds.setPassword(password);
			ds.setInitialSize(setInitialSize);
			ds.setMaxActive(setMaxActive);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	public static Connection getConnection() throws Exception {
		Connection conn = ds.getConnection();
		return conn;
		
	}
	
	public static void closeConnection(Connection conn) {
		if (conn!=null) {
			try {
				//璁剧疆鑷姩鎻愪氦鏄痶rue
				conn.setAutoCommit(true);
				conn.close();
			} catch (SQLException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	public static void rollBack(Connection conn){
		if (conn!=null) {
			try {
				conn.rollback();
			} catch (SQLException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

配置文件:

jdbc.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/demo2
jdbc.user=root
jdbc.password=123456
setInitialSize=2
setMaxActive=2

新建个线程模拟单个JDBC连接:

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;

import com.tedu.util.DBUtils;
public class MyRunnable implements Runnable{

	@Override
	public void run() {
		Connection conn = null;
		try {
			conn = DBUtils.getConnection();
			//关闭自动提交
			conn.setAutoCommit(false);
			//一些dml操作
			Statement sta = conn.createStatement();
			//1.查询订单
			String sql1 = "SELECT * FROM `order` WHERE `status` = 0";
			ResultSet result = sta.executeQuery(sql1);
			//判断查询到结果才执行2.变更订单状态
			if (result.next()) {
				String status = result.getString("status");
				int orderId = result.getInt("orderId");
				System.out.println(Thread.currentThread().getName() + "查询到了结果订单" + orderId + "支付状态为" + status);
				String sql2 = "UPDATE `order` SET `status` = 1 WHERE orderId = " + orderId;
				int executeUpdate = sta.executeUpdate(sql2);
				System.out.println(Thread.currentThread().getName() + "修改支付状态返回:" + executeUpdate);
				//2执行成功执行3.修改实收金额
				if (executeUpdate > 0) {
					String sql3 = "UPDATE `money` SET money = money + 500 WHERE orderId = " + orderId;
					int executeUpdate2 = sta.executeUpdate(sql3);
					System.out.println(Thread.currentThread().getName() + "修改实收金额状态返回:" + executeUpdate2);
					System.out.println(Thread.currentThread().getName() + "完成了操作支付");
				}
			}						
			//手动提交
			conn.commit();
		} catch (Exception e) {
			//回滚
			DBUtils.rollBack(conn);
			e.printStackTrace();
		}finally {
			DBUtils.closeConnection(conn);
		}		
	}
	
}

测试类:

public class MainTest {
		
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 2; i++) {
			Thread thread = new Thread(new MyRunnable());
			thread.start();		
		}
	}
}

运行看输出:

Thread-1查询到了结果订单1支付状态为0
Thread-1修改支付状态返回:1
Thread-0查询到了结果订单1支付状态为0
Thread-1修改实收金额状态返回:1
Thread-0修改支付状态返回:1
Thread-0修改实收金额状态返回:1
Thread-1完成了操作支付
Thread-0完成了操作支付

表中结果:
在这里插入图片描述
我们发现由于并发控制的问题导致我们多个JDBC连接对同一个订单重复累加了金额,这就是问题!

我们为了达到控制并发的目的,解决类似上述问题,人们提出了悲观锁和乐观锁的概念.着重说明乐观锁与悲观锁是人们定义出来的概念,可以认为是一种思想,而不要混淆为具体数据库中的某种锁。

一、悲观锁

悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能变动,一个事务拿到悲观锁后(可以理解为一个用户),其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。适用于写多读少的情况。
数据库中的行锁,表锁,读锁,写锁,以及syncronized实现的锁均为悲观锁。

我们可以利用for update实现悲观锁,代码如下
修改线程类我们在sql1的后边加上for update
在这里插入图片描述
运行测试类输出:

Thread-1查询到了结果订单1支付状态为0
Thread-1修改支付状态返回:1
Thread-1修改实收金额状态返回:1
Thread-1完成了操作支付

查看数据库money表:
在这里插入图片描述
我们发现并发可到了很好的控制,按照我们预期只有一个线程完成了支付状态修改,及金额实收操作。
注意:我们这里使用的mysql数据库,mysql中最常用的引擎是Innodb,Innodb默认使用的是行锁。而行锁是基于索引的,因此要想加上行锁,在加锁时必须命中索引,否则将使用表锁。

那么既然称为悲观锁,他的执行过程确实很悲观,只允许一个JDBC连接进行操作,另一个只能等待直到上一个占锁连接事务提交,显而易见这种锁并不适用于读操作很多的情况。

二、乐观锁

顾名思义,就是很乐观,每次自己操作数据的时候认为没有人会来修改它,所以不去加锁,但是在更新的时候回去判断在此期间数据有没有被修改过,需要用户自己去实现,不会发生并发抢占资源,只有在提交的时候检查是否为违反数据完整性。

乐观锁相比悲观锁,它允许多个连接同事对数据库操作,但是他会有其他方式保证多个事务顺序修改,常见的有增加一个时间戳字段或者增加一个版本号字段。

我们以增加版本号为例看乐观锁执行思路:
首先我们为order表增加字段version并初始化一条记录
在这里插入图片描述
初始化money表数据:
在这里插入图片描述

# 1. 查询未支付的订单(我们默认只会查出来一条,因为此时我们数据只有一条)
SELECT * FROM `order` WHERE orderId = 1 AND `status` = '0';
# 2.修改订单状态,并让版本号增加1,查询条件为步骤1.查询出来的orderId和版本号version
UPDATE `order` SET `status` = 1,`version` = `version` + 1 WHERE orderId = 0 and  `version` = 0;
#3.如果步骤2.执行成功则执行最后一步修改实收金额
UPDATE `money` SET money = money + 500 WHERE orderId = 1;

代码实现:
我们再新建两个线程类:

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;

import com.tedu.util.DBUtils;
public class MyRunnable2 implements Runnable{

	@Override
	public void run() {
		Connection conn = null;
		try {
			conn = DBUtils.getConnection();
			//关闭自动提交
			conn.setAutoCommit(false);
			//一些dml操作
			Statement sta = conn.createStatement();
			//1. 查询未支付的订单(我们默认只会查出来一条,因为此时我们数据只有一条)
			String sql1 = "SELECT * FROM `order` WHERE `status` = 0";
			ResultSet result = sta.executeQuery(sql1);
			//判断查询到结果才执行步骤2.变更订单状态
			if (result.next()) {
				//获取订单状态 0未支付 1已支付
				String status = result.getString("status");
				//获取订单号
				int orderId = result.getInt("orderId");
				//获取版本号
				int version = result.getInt("version");
				System.out.println(Thread.currentThread().getName() + "查询到了结果订单" + orderId + "支付状态为" + status + "版本号为" + version);
				//2.修改订单状态,并让版本号增加1,查询条件为步骤1.查询出来的orderId和版本号version
				String sql2 = "UPDATE `order` SET `status` = 1,`version` = `version` + 1 WHERE orderId = " + orderId + " and  `version` = " + version;
				int executeUpdate = sta.executeUpdate(sql2);
				System.out.println(Thread.currentThread().getName() + "修改支付状态返回:" + executeUpdate);
				//2执行成功执行3.修改实收金额
				if (executeUpdate > 0) {//表示修改成功
					String sql3 = "UPDATE `money` SET money = money + 500 WHERE orderId = " + orderId;
					int executeUpdate2 = sta.executeUpdate(sql3);
					System.out.println(Thread.currentThread().getName() + "修改实收金额状态返回:" + executeUpdate2);
					System.out.println(Thread.currentThread().getName() + "完成了操作支付");
				}
			}						
			//手动提交
			conn.commit();
		} catch (Exception e) {
			//回滚
			DBUtils.rollBack(conn);
			e.printStackTrace();
		}finally {
			DBUtils.closeConnection(conn);
		}		
	}
	
}

package com.tedu.test;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;

import com.tedu.util.DBUtils;
public class MyRunnable implements Runnable{

	@Override
	public void run() {
		Connection conn = null;
		try {
			conn = DBUtils.getConnection();
			//关闭自动提交
			conn.setAutoCommit(false);
			//一些dml操作
			Statement sta = conn.createStatement();
			//1.查询订单
			String sql1 = "SELECT * FROM `order` WHERE `status` = 0 for update";
			ResultSet result = sta.executeQuery(sql1);
			//判断查询到结果才执行2.变更订单状态
			if (result.next()) {
				String status = result.getString("status");
				int orderId = result.getInt("orderId");
				System.out.println(Thread.currentThread().getName() + "查询到了结果订单" + orderId + "支付状态为" + status);
				String sql2 = "UPDATE `order` SET `status` = 1 WHERE orderId = " + orderId;
				int executeUpdate = sta.executeUpdate(sql2);
				System.out.println("=====" + Thread.currentThread().getName() + "修改支付状态返回:" + executeUpdate);
				/*
				*注意这里特别重要,如果是该线程先执行的
				*update,那么该处休眠10秒可以验证
				*两个未提交的事务,其中先执行update的会对事
				*务上锁,另一事务运行到该代码处被阻塞直到先执
				*行的提交事务
				*mysql默认隔离级别可重复读
				*/
				Thread.currentThread().sleep(10000);
				//2执行成功执行3.修改实收金额
				if (executeUpdate > 0) {
					String sql3 = "UPDATE `money` SET money = money + 500 WHERE orderId = " + orderId;
					int executeUpdate2 = sta.executeUpdate(sql3);
					System.out.println(Thread.currentThread().getName() + "修改实收金额状态返回:" + executeUpdate2);
					System.out.println(Thread.currentThread().getName() + "完成了操作支付");
				}
			}						
			//手动提交
			conn.commit();
		} catch (Exception e) {
			//回滚
			DBUtils.rollBack(conn);
			e.printStackTrace();
		}finally {
			DBUtils.closeConnection(conn);
		}		
	}
	
}

执行测试方法输出:

Thread-0查询到了结果订单1支付状态为0版本号为0
Thread-1查询到了结果订单1支付状态为0版本号为0
=====Thread-0修改支付状态返回:1
Thread-0修改实收金额状态返回:1
Thread-0完成了操作支付
//此处停顿了10秒
Thread-1修改支付状态返回:0

由输出我们可以看到两个线程(JDBC连接)都查询到了待处理订单,但是0线程优先(随机)执行了步骤2操作,此时线程1在执行到第一个update语句时阻塞,直到0线程提交了事务步骤2继续执行发现where条件中version(1) <>version(0)而导致update执行失败也就是输出了 “Thread-1修改支付状态返回:0”。
我们再看数据库验证:
先看订单表:

在这里插入图片描述
支付状态改为了1已支付,版本号相应的+1操作0->1
再看money表:
在这里插入图片描述
金额也是正确的。

综上阐述乐观锁具体实现细节。主要就是两个步骤:冲突检测和数据更新。

以上只是对乐观锁与悲观锁的简单介绍,具体业务还要具体分析。其实在这些sql上还有很大的优化空间。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页