MySQL,作为广泛使用的开源关系型数据库管理系统,提供了多种并发控制手段,其中乐观锁和悲观锁是两种尤为重要的锁机制
本文将深入探讨这两种锁机制的工作原理、优缺点以及适用场景,并通过实例说明它们在实际开发中的应用
一、乐观锁:基于假设的并发控制 乐观锁,顾名思义,是一种基于乐观假设的并发控制机制
它假设在大多数情况下,并发事务之间不会发生冲突,因此在事务开始时不会对数据进行加锁
相反,乐观锁在提交数据更新之前,会检查数据是否在事务执行期间被其他事务修改过
这一检查通常通过比较数据的版本号或时间戳来实现
1.1 工作原理 乐观锁的工作原理可以概括为以下几个步骤: 1.读取数据:事务开始时,首先读取所需数据及其版本号或时间戳
2.业务处理:在内存中执行必要的业务逻辑处理
3.检查冲突:在提交更新之前,检查数据库中数据的版本号或时间戳是否与读取时一致
4.提交更新:如果版本号或时间戳一致,说明数据未被其他事务修改,允许提交更新,并更新版本号或时间戳
如果不一致,则说明数据已被其他事务修改,此时根据业务逻辑决定是重试更新操作还是抛出异常
1.2 优缺点 乐观锁的优点主要体现在以下几个方面: - 提高并发性能:由于事务开始时不对数据进行加锁,因此可以显著提高系统的并发性能
- 避免死锁:乐观锁不需要显式地申请和释放锁,因此可以避免死锁问题的发生
然而,乐观锁也存在一些缺点: - 冲突处理复杂:当数据冲突发生时,需要额外的逻辑来处理重试或异常
- 无法保证强一致性:在高并发环境下,乐观锁可能无法保证数据的强一致性,只能达到最终一致性
1.3 适用场景 乐观锁适用于读多写少的场景,以及对数据一致性要求相对较低的情况
例如,在电商系统中,商品的浏览次数统计就是一个典型的读多写少的操作
多个用户可以同时查看商品详情页面,而对浏览次数的更新相对较少
此时,使用乐观锁可以显著提高系统的并发性能
二、悲观锁:谨慎的并发控制策略 与乐观锁相反,悲观锁持一种悲观的态度,认为在并发环境中,数据很可能会被其他事务修改
因此,在事务开始时,就对要操作的数据进行加锁,直到事务完成并提交或回滚后才释放锁
这样可以确保在事务执行期间,其他事务无法对锁定的数据进行修改操作
2.1 工作原理 悲观锁的工作原理相对简单直接: 1.申请锁:事务开始时,通过SQL语句显式地申请对数据行的锁
2.执行操作:在持有锁的情况下,执行必要的数据库操作
3.释放锁:事务完成后,释放锁以允许其他事务访问被锁定的数据
悲观锁主要有两种类型:共享锁(Shared Lock)和排他锁(Exclusive Lock)
共享锁允许多个事务同时读取数据,但不允许修改数据;而排他锁则只允许一个事务访问数据,既可以读取也可以修改
2.2 优缺点 悲观锁的优点在于: - 实现简单:悲观锁通过显式地申请和释放锁来控制并发访问,实现起来相对简单
- 保证数据一致性:悲观锁可以确保在事务执行期间,数据不会被其他事务修改,从而保证了数据的一致性
然而,悲观锁也存在一些显著的缺点: - 降低并发性能:由于事务开始时就需要对数据加锁,因此会阻塞其他事务的访问,降低了系统的并发性能
- 可能导致死锁:在复杂的事务场景中,悲观锁可能会导致死锁问题的发生
2.3 适用场景 悲观锁适用于写多读少的场景,以及对数据一致性要求极高的情况
例如,在银行系统的转账操作中,涉及到对账户余额的修改,必须确保在整个转账事务过程中,账户余额数据不会被其他并发事务干扰
此时,使用悲观锁可以有效避免数据不一致的问题
三、实例分析:乐观锁与悲观锁的应用 为了更好地理解乐观锁和悲观锁在实际开发中的应用,以下将通过具体的实例进行分析
3.1 乐观锁实例 假设我们有一个商品表`products`,其中包含`id`(主键)、`name`、`quantity`和`version`字段
`version`字段将作为乐观锁的版本号
以下是在Java代码中使用乐观锁更新商品数量的示例: // 数据库连接信息 String url = jdbc:mysql://localhost:3306/mydb; String username = root; String password = password; try (Connection connection = DriverManager.getConnection(url, username,password)){ // 开启事务 connection.setAutoCommit(false); // 查询商品信息并获取当前版本号 String selectSql = SELECT quantity, version FROM products WHERE id = ?; try(PreparedStatement selectStmt = connection.prepareStatement(selectSql)) { selectStmt.setInt(1, 1); // 假设要更新id为1的商品 try(ResultSet resultSet = selectStmt.executeQuery()) { if(resultSet.next()) { int currentQuantity = resultSet.getInt(quantity); int currentVersion = resultSet.getInt(version); // 模拟业务逻辑,减少商品数量 int newQuantity = currentQuantity - 1; // 更新商品数量和版本号,使用乐观锁机制 String updateSql = UPDATE products SET quantity = ?, version = version + 1 WHERE id = ? AND version = ?; try(PreparedStatement updateStmt = connection.prepareStatement(updateSql)) { updateStmt.setInt(1, newQuantity); updateStmt.setInt(2, 1); // 商品id updateStmt.setInt(3, currentVersion); int rowsUpdated = updateStmt.executeUpdate(); if (rowsUpdated == { // 说明在更新之前数据被其他事务修改,乐观锁冲突 System.out.println(乐观锁冲突,更新失败); connection.rollback(); } else{ // 提交事务 connection.commit(); System.out.println(商品数量更新成功); } } } } } } catch(SQLExceptione){ e.printStackTrace(); } 在上述代码中,首先查询商品的当前数量和版本号,然后根据业务需求计算新的数量
在更新时,通过`WHERE`子句中的版本号条件来检查数据是否被其他事务修改
如果更新的行数为0,则表示发生了乐观锁冲突
3.2 悲观锁实例 同样以`products`表为例,在MySQL中可以使用`SELECT ... FORUPDATE`语句来实现悲观锁
以下是一个使用悲观锁查询商品信息并锁定行的示例: -- 开启事务 START TRANSACTION; -- 使用悲观锁查询商品信息并锁定行 SELECT quantity FROM products WHERE id = 1 FOR UPDATE; -- 模拟业务逻辑,减少商品数量 UPDATE products SET quantity = quantity - 1 WHERE id = 1; -- 提交事务 COMMIT; 在上述SQL代码中,`SELECT quantity FROM products WHERE id = 1 FOR UPDATE`语句会对`id`为1的商品行进行锁定,直到事务提交或回滚
在锁定期间,其他事务如果尝试对该行数据进行修改操作,将会被阻塞,直到锁被释放
四、结论 乐观锁和悲观锁是My