真实生产事故情形:
代码在正常情况下不会触发,只有在某一个字段(到期月)是节假日时候才会进入一个循环,这时候程序在运行中,该用户进来在操作数据库中该条数据,进入循环读修改,并持续,这时候由另一位用户进来,同时在读修改这条数据,会导致表被锁!如果这张表是业务表,则会导致之后的业务无法进行,只能通过数据库服务器使用命令去批量解锁,释放锁后才能继续之后的业务操作
问题分析:
简单来说,就是多个用户同时去修改一个数据会导致并发问题,其中数据库系统会使用锁来确保数据的一致性和事务隔离。会出现,行级锁,表级锁,共享锁,死锁
当一个表被锁定时,其他业务场景可能会受到影响,具体影响的程度取决于锁的类型和锁定的范围。在数据库中,锁有不同的粒度,包括行级锁、表级锁等。不同的数据库管理系统和事务隔离级别也会对锁的行为产生影响。
以下是一些可能的情况:
-
行级锁: 如果一个事务在表中的某些行上获得了排它锁(写锁),其他事务可能会被阻塞,直到该锁被释放。这可能导致其他业务无法在被锁定的行上执行更新或插入操作。
-
表级锁: 如果一个事务在整个表上获得了排它锁,其他事务可能被阻塞,无法在该表上执行任何写操作。这将导致其他业务无法修改表的内容,直到锁被释放。
-
共享锁: 如果一个事务在表或行上获得了共享锁,其他事务可以获取相同的共享锁,但排它锁将被阻塞。这可能导致其他业务无法在被锁定的资源上执行写操作。
-
死锁: 如果多个事务相互等待对方释放锁定资源,可能会发生死锁。这将导致参与死锁的事务之一被中止,从而影响业务的正常进行。
在一个数据库中,当多个用户同时访问并修改相同的数据时,可能会发生数据竞争和数据库表的锁定。这取决于数据库管理系统(DBMS)的实现和事务隔离级别。
在事务隔离级别中,有四个标准级别,它们定义了事务在同时运行时对数据的可见性和互相影响的程度。这些级别是:
1、READ UNCOMMITTED (读未提交): 允许一个事务读取另一个事务未提交的数据。可能会发生脏读、不可重复读和幻读。
2、READ COMMITTED (读已提交): 允许一个事务读取另一个事务已提交的数据。可以避免脏读,但可能发生不可重复读和幻读。
3、REPEATABLE READ (可重复读): 保证一个事务在其生命周期内多次读取相同的数据将得到相同的结果。可以避免脏读和不可重复读,但可能发生幻读。
4、SERIALIZABLE (串行化): 最高的隔离级别,确保事务的串行执行。可以避免脏读、不可重复读和幻读,但性能可能较低。
如果两个用户同时修改相同的数据,并且事务隔离级别较高(例如,REPEATABLE READ或SERIALIZABLE),那么一个用户的事务可能会等待另一个用户的事务完成,以避免并发修改问题。这种等待可能导致表锁定,具体取决于DBMS的实现。
在Java中,使用JDBC或类似的持久性框架时,可以考虑通过事务管理来控制隔离级别,以及处理并发访问问题。在使用数据库时,确保了解数据库的默认隔离级别和如何更改它,以及了解事务的提交和回滚对并发的影响。
解决建议:合理使用锁和其他一些并发控制的技术和策略
1、合理使用事务: 将数据库操作组织成事务,确保事务的执行时间尽可能短。长时间运行的事务可能导致锁定资源的时间较长,增加了发生锁冲突的可能性。确保在事务中只涉及必要的数据,并且只在必要的时候开启事务。
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false); // 开始事务
// 执行数据库操作
connection.commit(); // 提交事务
} catch (SQLException e) {
// 处理异常,回滚事务
}
2、适当设置事务隔离级别: 根据业务需求,选择适当的事务隔离级别。较低的隔离级别(如READ COMMITTED)通常具有更高的并发性,但可能导致一些并发问题。较高的隔离级别(如SERIALIZABLE)提供更好的一致性,但可能降低并发性。
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
3、悲观锁的谨慎使用: 在悲观锁的使用中,确保只在必要的时候使用。悲观锁在并发性能方面可能会有一些影响,因此只在必要的时候使用,例如在对某些关键数据进行写入时。
4、合理设计索引: 使用适当的索引来提高查询效率,减少锁的争夺。过多的索引可能导致性能问题,因此需要权衡。
5、避免大批量更新: 将大批量更新操作拆分为较小的批次,以减少锁定的范围和持有锁的时间。
6、定期监控性能: 定期监控数据库性能,识别潜在的锁定问题,并根据需要进行调整。数据库管理系统通常提供了性能监控和锁定信息的工具。
7、并发控制框架的使用: 一些框架和库提供了高级的并发控制工具,可以简化在代码中实现并发控制策略。例如,在Spring框架中,可以使用@Transactional
注解管理事务。
@Transactional(isolation = Isolation.READ_COMMITTED)
public void businessLogic() {
// 编写正常的业务逻辑
}
最简便的就是使用@Transactional注解去管理事务,只需在方法上面加上注解,就可以让该方法自动被事务管理起来,并且可以配置它的策略,缺点也很明显,如果不使用spring框架,就无法使用,对特定数据库特性的局限性, 一些数据库特性可能需要更精细的事务控制,而该注解提供的配置可能无法满足这些特殊需求。
并发控制的技术和策略:
-
版本控制(Optimistic Concurrency Control): 乐观锁的一种实现方式,基于数据版本号或时间戳。每个事务在读取数据时获取版本信息,并在提交时检查数据是否被其他事务修改。如果发现冲突,事务可能需要回滚并重新尝试。
-
行级锁(Row-level Locking): 事务可以锁定数据表中的特定行,以防止其他事务同时修改相同的行。这是悲观锁的一种实现方式,通常由数据库管理系统自动处理。
-
分布式锁(Distributed Locking): 用于分布式系统的锁定机制,确保在多个节点上的事务之间保持一致性。分布式锁需要考虑更复杂的问题,如网络延迟和节点故障。
-
两阶段锁协议(Two-Phase Locking Protocol): 用于管理并发事务的一致性的协议。包括“生长阶段”和“收缩阶段”,其中在生长阶段获得锁,而在收缩阶段释放锁。该协议有助于避免死锁。
-
写时复制(Copy-On-Write): 一种乐观锁的实现方式,当事务试图修改数据时,首先创建数据的副本,然后在副本上进行修改。这样其他事务可以继续读取原始数据,直到修改完成。
-
乐观并发控制框架: 一些现代数据库和框架提供了专门的乐观并发控制工具,以简化在代码中实现乐观锁。这些框架通常会处理版本号或时间戳的生成和比较。
选择适当的并发控制机制取决于应用程序的特定要求、数据库管理系统的支持以及性能需求。在设计数据库操作时,仔细考虑数据的访问模式、并发性要求和一致性需求,以选择最合适的策略。
注意:
dataSource.getConnection()
注:文章有哪里写得不对,欢迎大家在评论区讨论,互相学习,共同进步,感谢大家!