文章目录
什么是事物?
事务是访问数据库的一个操作序列,数据库应用系统通过事务集来完成对数据库的存取。事务的正确执行使得数据库从一种状态转换为另一种状态。
事务必须服从ISO/IEC所制定的ACID原则。ACID是原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)的缩写,这四种状态的意思是:
1、原子性
即不可分割,事务要么全部被执行,要么全部不执行。如果事务的所有子事务全部提交成功,则所有的数据库操作被提交,数据库状态发生变化;如果有子事务失败,则其他子事务的数据库操作被回滚,即数据库回到事务执行前的状态,不会发生状态转换
2、一致性
事务的执行使得数据库从一种正确状态转换成另外一种正确状态
3、隔离性
在事务正确提交之前,不允许把事务对该数据的改变提供给任何其他事务,即在事务正确提交之前,它可能的结果不应该显示给其他事务
4、持久性
事务正确提交之后,其结果将永远保存在数据库之中,即使在事务提交之后有了其他故障,事务的处理结果也会得到保存
事务的作用
事务管理对于企业级应用而言至关重要,它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性。就像银行的自动提款机ATM,通常ATM都可以正常为客户服务,但是也难免遇到操作过程中及其突然出故障的情况,此时,事务就必须确保出故障前对账户的操作不生效,就像用户刚才完全没有使用过ATM机一样,以保证用户和银行的利益都不受损失。
spring事务与数据库事务与锁之间的关系
spring事务:
spring事务本质上使用数据库事务,而数据库事务本质上使用数据库锁,所以spring事务本质上使用数据库锁,开启spring事务意味着使用数据库锁;
数据库是可以控制事务的传播和隔离级别的,Spring在之上又进一步对一致性、隔离性进行了封装,可以在不同的项目、不同的操作中再次对事务的传播行为和隔离级别进行策略控制。
注意:Spring不仅可以控制事务传播行为(PROPAGATION_REQUIRED等),还可以控制事务隔离级别(ISOLATION_READ_UNCOMMITTED等)。
那么事务的隔离级别与锁有什么关系呢?本人认为事务的隔离级别是通过锁的机制实现的,事务的隔离级别是数据库开发商根据业务逻辑的实际需要定义的一组锁的使用策略。当我们将数据库的隔离级别定义为某一级别后如仍不能满足要求,我们可以自定义 sql 的锁来覆盖事务隔离级别默认的锁机制。
spring事务实际使用AOP拦截注解方法,然后使用动态代理处理事务方法,捕获处理过程中的异常,spring事务其实是把异常交给spring处理;
spring事务只有捕获到异常才会终止或回滚,如果你在程序中try/catch后自己处理异常而没有throw,那么事务将不会终止或回滚,失去事务本来的作用;
spring事务会捕获所有的异常,但只会回滚数据库相关的操作,并且只有在声明了rollbackForClassName="Exception"之类的配置才会回滚;
spring事务会回滚同一事务中的所有数据库操作,本质上是回滚同一数据库连接上的数据库操作;
spring事务总结:
spring事务本质上使用数据库锁;
spring事务只有在方法执行过程中出现异常才会回滚,并且只回滚数据库相关的操作;
对象锁和spring事务的对比:
对象锁可以保证数据一致性和业务逻辑正确性,但不能保证并发性;
spring事务不能严格保证数据一致性和业务逻辑正确性,但具有较好的并发性,因为只锁数据库行数据;
建议:
如果只有insert操作,可以使用事务;
如果涉及update操作但不涉及其他业务逻辑,可以保守使用事务;
如果涉及update操作及其他业务逻辑,慎用事务,
并且数据库查询跟数据库更新之间尽量间隔较短,中间不宜插入太多其他逻辑,减少数据一致性的风险;
对数据一致性要求不高的情况下可以使用事务结合乐观锁,否则建议用锁;
spring事务为什么不能保证数据一致性和业务逻辑正确性:
1.如果事务方法抛异常,此时会回滚数据库操作,但已经执行的其他方法不会回滚,因此无法保证业务逻辑正确性;
2.即使事务方法不抛异常,也不能保证数据一致性(因为事务接口里的数据库操作在整个接口逻辑执行结束后才提交到数据 库,在接口最后提交到数据库的前后很有可能带来数据一致性的问题),从而不能保证业务逻辑正确性;
编程式事务和声明式事务区别
编程式事务: 需要你在代码中直接加入处理事务的逻辑,可能需要在代码中显式调用beginTransaction()、commit()、rollback()等事务管理相关的方法,如在执行a方法时候需要事务处理,你需要在a方法开始时候开启事务,处理完后。在方法结束时候,关闭事务.
编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
声明式的事务:
声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。
声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,
声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。
声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。
二者区别.编程式事务侵入性比较强,但处理粒度更细.
spring编程式事物
引入事务管理器
@Autowired
TransactionTemplate transactionTemplate;
@Autowired
PlatformTransactionManager transactionManager;
使用方式1
//开启事务保存数据
boolean result = transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
try {
// TODO something
} catch (Exception e) {
//TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); //手动开启事务回滚
status.setRollbackOnly();
logger.error(e.getMessage(), e);
return false;
}
return true;
}
});
使用方式2
/**
* 定义事务
*/
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setReadOnly(false);
//隔离级别,-1表示使用数据库默认级别
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
//TODO something
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw new InvoiceApplyException("异常失败");
}
使用方式3
SqlSession sqlSession = null;
try {
sqlSession = otInvSqlSessionFactory.openSession(ExecutorType.BATCH, true);
XXXXXMapper xXxxMapper = sqlSession.getMapper(XXXXXMapper.class);
sqlSession.commit();
}catch(Exception e){
if (null != otInvSqlSession) {
sqlSession.rollback(true);
logger.error("数据回滚", e);
}
}finally {
if (null != sqlSession) {
sqlSession.clearCache();
sqlSession.close();
}
}
Spring 声明式事务实例
Spring 支持声明式事务,即使用注解来选择需要使用事务的方法,他使用 @Transactional 注解在方法上表明该方法需要事务支持。被注解的方法在被调用时,Spring 开启一个新的事务,当方法无异常运行结束后,Spring 会提交这个事务。如:
@Transactional
public void saveStudent(Student student){
// 数据库操作
}
注意,@Transactional 注解来自于 org.springframework.transcation.annotation 包,而不是 javax.transaction。
Spring 提供一个 @EnableTranscationManagement 注解在配置类上来开启声明式事务的支持。使用了 @EnableTranscationManagement 后,Spring 容器会自动扫描注解 @Transactional 的方法与类。@EnableTranscationManagement 的使用方式如下:
@Configuration
@EnableTranscationManagement
public class AppConfig{
}
注解事务行为
@Transactional 有如下表所示的属性来定制事务行为。
属性 含义
propagation 事务传播行为
isolation 事务隔离级别
readOnly 事务的读写性,boolean型
timeout 超时时间,int型,以秒为单位。
rollbackFor 一组异常类,遇到时回滚。(rollbackFor={SQLException.class})
rollbackForCalssName 一组异常类名,遇到回滚,类型为 string[]
noRollbackFor 一组异常类,遇到不回滚
norollbackForCalssName 一组异常类名,遇到时不回滚。
类级别使用 @Transactional
@Transactional 不仅可以注解在方法上,还可以注解在类上。注解在类上时意味着此类的所有 public 方法都是开启事务的。如果类级别和方法级别同时使用了 @Transactional 注解,则使用在类级别的注解会重载方法级别的注解。
SpringBoot 的事务支持
自动配置的事务管理器
在使用 JDBC 作为数据访问技术时,配置定义如下:
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(DataSource.class)
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(this.dataSource)
}
在使用 JPA 作为数据访问技术时,配置定义如下:@Bean
@ConditionalOnMissingBean(PlatformTransactionManager.class)
public PlatformTransactionManager transactionManager(){
return new JpaTransactionManager()
}
自动开启注解事务的支持
SpringBoot 专门用于配置事务的类为 org.springframework.boot.autoconfigure.transcation.TransactionAutoConfiguration,此配置类依赖于 JpaBaseConfiguration 和 DataSourceTransactionManagerAutoConfiguration。
而在 DataSourceTransactionManagerAutoConfiguration 配置里还开启了对声明式事务的支持,代码如下:
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
@Configuration
@EnableTransactionManagement
protected static class TransactionManagementConfiguration{
}
SpringBoot 中,无须显式开启使用 @EnableTransactionManagement 注解。
核心代码:
import com.nasus.transaction.domain.Student;
import com.nasus.transaction.repository.StudentRepository;
import com.nasus.transaction.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
// 直接注入 StudentRepository 的 bean
private StudentRepository studentRepository;
// 使用 @Transactional 注解的 rollbackFor 属性,指定特定异常时,触发回滚
@Transactional(rollbackFor = {IllegalArgumentException.class})
@Override
public Student saveStudentWithRollBack(Student student) {
Student s = studentRepository.save(student);
if ("高斯林".equals(s.getName())){
//硬编码,手动触发异常
throw new IllegalArgumentException("高斯林已存在,数据将回滚");
}
return s;
}
// 使用 @Transactional 注解的 noRollbackFor 属性,指定特定异常时,不触发回滚
@Transactional(noRollbackFor = {IllegalArgumentException.class})
@Override
public Student saveStudentWithoutRollBack(Student student) {
Student s = studentRepository.save(student);
if ("高斯林".equals(s.getName())){
throw new IllegalArgumentException("高斯林已存在,数据将不会回滚");
}
return s;
}
}
Spring事物的传播行为
什么叫事务传播行为?听起来挺高端的,其实很简单。
即然是传播,那么至少有两个东西,才可以发生传播。单体不存在传播这个行为。
事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。
Spring定义了七种传播行为:
事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
现在来看看传播行为
1、PROPAGATION_REQUIRED
如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
可以把事务想像成一个胶囊,在这个场景下方法B用的是方法A产生的胶囊(事务)。
举例有两个方法:
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// do something
}
单独调用methodB方法时,因为当前上下文不存在事务,所以会开启一个新的事务。
调用methodA方法时,因为当前上下文不存在事务,所以会开启一个新的事务。当执行到methodB时,methodB发现当前上下文有事务,因此就加入到当前事务中来。
2、PROPAGATION_SUPPORTS
如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
举例有两个方法:
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 事务属性为SUPPORTS
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
// do something
}
单纯的调用methodB时,methodB方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中,事务地执行。
3、PROPAGATION_MANDATORY
如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 事务属性为MANDATORY
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
// do something
}
当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);当调用methodA时,methodB则加入到methodA的事务中,事务地执行。
4、PROPAGATION_REQUIRES_NEW
使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作为事务管理器。
它会开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
doSomeThingA();
methodB();
doSomeThingB();
// do something else
}
// 事务属性为REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// do something
}
当调用
main{
methodA();
}
相当于调用
main(){
TransactionManager tm = null;
try{
//获得一个JTA事务管理器
tm = getTransactionManager();
tm.begin();//开启一个新的事务
Transaction ts1 = tm.getTransaction();
doSomeThing();
tm.suspend();//挂起当前事务
try{
tm.begin();//重新开启第二个事务
Transaction ts2 = tm.getTransaction();
methodB();
ts2.commit();//提交第二个事务
} Catch(RunTimeException ex) {
ts2.rollback();//回滚第二个事务
} finally {
//释放资源
}
//methodB执行完后,恢复第一个事务
tm.resume(ts1);
doSomeThingB();
ts1.commit();//提交第一个事务
} catch(RunTimeException ex) {
ts1.rollback();//回滚第一个事务
} finally {
//释放资源
}
}
在这里,我把ts1称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2与ts1是两个独立的事务,互不相干。Ts2是否成功并不依赖于 ts1。如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。而除了 methodB之外的其它代码导致的结果却被回滚了
5、PROPAGATION_NOT_SUPPORTED
PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作为事务管理器。
6、PROPAGATION_NEVER
总是非事务地执行,如果存在一个活动事务,则抛出异常。
7、PROPAGATION_NESTED
如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。
这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。
需要JDBC 驱动的java.sql.Savepoint类。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true(属性值默认为false)。
这里关键是嵌套执行。
@Transactional(propagation = Propagation.REQUIRED)
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}
@Transactional(propagation = Propagation.NEWSTED)
methodB(){
……
}
如果单独调用methodB方法,则按REQUIRED属性执行。如果调用methodA方法,相当于下面的效果:
main(){
Connection con = null;
Savepoint savepoint = null;
try{
con = getConnection();
con.setAutoCommit(false);
doSomeThingA();
savepoint = con2.setSavepoint();
try{
methodB();
} catch(RuntimeException ex) {
con.rollback(savepoint);
} finally {
//释放资源
}
doSomeThingB();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
//释放资源
}
}
当methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括methodB方法的所有操作。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:
它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。
使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。
使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTATrasactionManager实现可能有不同的支持方式。
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。
由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back.
事物的隔离级别
事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大,因此很多时候必须在并发性和性能之间做一个权衡。所以设立了几种事务隔离级别,以便让不同的项目可以根据自己项目的并发情况选择合适的事务隔离级别,对于在事务隔离级别之外会产生的并发问题,在代码中做补偿。
事务隔离级别有4种,但是像Spring会提供给用户5种,来看一下:
1、DEFAULT
默认隔离级别,每种数据库支持的事务隔离级别不一样,如果Spring配置事务时将isolation设置为这个值的话,那么将使用底层数据库的默认事务隔离级别。顺便说一句,如果使用的MySQL,可以使用"select @@tx_isolation"来查看默认的事务隔离级别
2、READ_UNCOMMITTED
读未提交,即能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种,因此很少使用
所谓脏读(读未提交),就是指事务A读到了事务B还没有提交的数据,比如银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务–>取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。
3、READ_COMMITED
读已提交,即能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读
所谓不可重复读 (读已提交),就是指在一个事务里面读取了两次某个数据,读出来的数据不一致。还是以银行取钱为例,事务A开启事务–>查出银行卡余额为1000元,此时切换到事务B事务B开启事务–>事务B取走100元–>提交,数据库里面余额变为900元,此时切换回事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。
4、REPEATABLE_READ
重复读取,即在数据读出来之后加锁,类似"select * from XXX for update",明确数据读取出来就是为了更新用的,所以要加一把锁,防止别人修改它。REPEATABLE_READ的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决
所谓幻读,就是指在一个事务里面的操作中发现了未被操作的数据。比如学生信息,事务A开启事务–>修改所有学生当天签到状况为false,此时切换到事务B,事务B开启事务–>事务B插入了一条学生数据,此时切换回事务A,事务A提交的时候发现了一条自己没有修改过的数据,这就是幻读,就好像发生了幻觉一样。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。
5、SERLALIZABLE
串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了
网上专门有图用表格的形式列出了事务隔离级别解决的并发问题:
read uncommited:是最低的事务隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。
read commited:保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。
repeatable read:这种事务隔离级别可以防止脏读,不可重复读。但是可能会出现幻象读。它除了保证一个事务不能被另外一个事务读取未提交的数据之外还避免了以下情况产生(不可重复读)。
serializable:这是花费最高代价但最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读之外,还避免了幻象读(避免三种)。
Mysql 默认:Repeatable read 可重复读
Oracle 默认:Read committed 读已提交
SqlServer 默认:Read committed 读已提交
补充:
1、SQL规范所规定的标准,不同的数据库具体的实现可能会有些差异
2、mysql中默认事务隔离级别是可重复读时并不会锁住读取到的行
3、事务隔离级别为读提交时,写数据只会锁住相应的行
4、事务隔离级别为可重复读时,如果有索引(包括主键索引)的时候,以索引列为条件更新数据,会存在间隙锁间隙锁、行锁、下一键锁的问题,从而锁住一些行;如果没有索引,更新数据时会锁住整张表。
5、事务隔离级别为串行化时,读写数据都会锁住整张表
6、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
事务隔离级别设置得越高,意味着势必要花手段去加锁用以保证事务的正确性,那么效率就要降低,因此实际开发中往往要在效率和并发正确性之间做一个取舍,一般情况下会设置为READ_COMMITED,,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
mysql事务隔离级别与spring事务隔离级别的区别
数据库隔离级别有四种
Spring事务隔离级别
ISOLATION_DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应 。
ISOLATION_READ_UNCOMMITTED 这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读
ISOLATION_READ_COMMITTED 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。
ISOLATION_REPEATABLE_READ 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。
ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。
mysql事务隔离级别与spring事务隔离级别的区别:
脏读:为什么会出现脏读,因为你对数据库的任何修改都会是立即生效的,至于别人能不能看到主要取决与你 是否加锁了,数据库的执行与事务没有关系,事务只是保证对数据库所做的操作会不会撤销而已,mysql默认是行级锁,修改时只会对修改的那几行加锁,加锁期间其他用户线程是看不到修改结果的,所以会导致不可重复读和幻读的问题;
mysql的事务隔离是通过行级锁实现的;
oracle的行级锁只有排他锁没有共享锁,默认不是使用锁机制来实现隔离的,而是通过版本控制,不会脏读但是会出现重复读问题,因为a线程在修改期间没有加锁,b线程仍然可以读取到
幻读的原因:
幻读的解释:幻读是因为a线程在一定范围内对一批数据进行修改,b线程新增了一条数据,导致用户以为自己没有全量修改;
幻读的原因:因为mysql默认是通过行级锁来实现的,而不是表级锁,那么在修改期间其他线程当然可以新插入数据了;如果使用了mysql表级锁的引擎如:MyIsAM,幻读就不可能出现;
spring的隔离级别:
spring的隔离级别一定是数据库锁机制支持的;如果数据库没有行级锁机制,也没有版本控制那么spring也没办法;
脏读和不可重复读的区别在于读的时候是否加了读共享锁,不可重复读加了读共享锁;脏读是写的时候加了排他锁
spring和数据库(mysql)隔离级别的比较:
spring管理的事务是逻辑事务,而物理事务和逻辑事务的区别在于 事务的传播行为,spring隔离级别和数据库是对应的,在做dml时,数据在数据库中其实已经修改了,只是没有生效而且由于多线程锁的关系,其他的线程过来是看不到该dml操作的结果的。
乐观锁、悲观锁
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
4.2 数据库的锁机制及原理
先看一张图自己整理的数据库锁的树形图
概要
数据库锁一般可以分为两类,一个是悲观锁,一个是乐观锁。
乐观锁一般是指用户自己实现的一种锁机制,假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。乐观锁的实现方式一般包括使用版本号和时间戳。
悲观锁一般就是我们通常说的数据库锁机制,以下讨论都是基于悲观锁。
悲观锁主要表锁、行锁、页锁。在MyISAM中只用到表锁,不会有死锁的问题,锁的开销也很小,但是相应的并发能力很差。innodb实现了行级锁和表锁,锁的粒度变小了,并发能力变强,但是相应的锁的开销变大,很有可能出现死锁。同时inodb需要协调这两种锁,算法也变得复杂。InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
表锁和行锁都分为共享锁和排他锁(独占锁),而更新锁是为了解决行锁升级(共享锁升级为独占锁)的死锁问题。
innodb中表锁和行锁一起用,所以为了提高效率才会有意向锁(意向共享锁和意向排他锁)。
为了表锁和行锁而存在的意向锁
官方文档中是这么描述的,
Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table
知乎上有个解释十分形象,如下:
1, 在mysql中有表锁,读锁锁表,会阻塞其他事务修改表数据。写锁锁表,会阻塞其他事务读和写。
2,Innodb引擎又支持行锁,行锁分为共享锁,一个事务对一行的共享只读锁。排它锁,一个事务对一行的排他读写锁。
3,这两中类型的锁共存的问题考虑这个例子:事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。
数据库要怎么判断这个冲突呢?
step1:判断表是否已被其他事务用表锁锁表
step2:判断表中的每一行是否已被行锁锁住。
注意step2,这样的判断方法效率实在不高,因为需要遍历整个表。于是就有了意向锁。在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。
在意向锁存在的情况下,上面的判断可以改成
step1:不变
step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。
行锁的细分
共享锁
加锁与解锁:当一个事务执行select语句时,数据库系统会为这个事务分配一把共享锁,来锁定被查询的数据。在默认情况下,数据被读取后,数据库系统立即解除共享锁。例如,当一个事务执行查询“SELECT * FROM accounts”语句时,数据库系统首先锁定第一行,读取之后,解除对第一行的锁定,然后锁定第二行。这样,在一个事务读操作过程中,允许其他事务同时更新accounts表中未锁定的行。
兼容性:如果数据资源上放置了共享锁,还能再放置共享锁和更新锁。
并发性能:具有良好的并发性能,当数据被放置共享锁后,还可以再放置共享锁或更新锁。所以并发性能很好。
排他锁
加锁与解锁:当一个事务执行insert、update或delete语句时,数据库系统会自动对SQL语句操纵的数据资源使用独占锁。如果该数据资源已经有其他锁(任何锁)存在时,就无法对其再放置独占锁了。
兼容性:独占锁不能和其他锁兼容,如果数据资源上已经加了独占锁,就不能再放置其他的锁了。同样,如果数据资源上已经放置了其他锁,那么也就不能再放置独占锁了。
并发性能:最差。只允许一个事务访问锁定的数据,如果其他事务也需要访问该数据,就必须等待。
更新锁
更新锁在的初始化阶段用来锁定可能要被修改的资源,这可以避免使用共享锁造成的死锁现象。例如,对于以下的update语句:
UPDATE accounts SET balance=900 WHERE id=1
更新操作需要分两步:读取accounts表中id为1的记录 –> 执行更新操作。
如果在第一步使用共享锁,再第二步把锁升级为独占锁,就可能出现死锁现象。例如:两个事务都获取了同一数据资源的共享锁,然后都要把锁升级为独占锁,但需要等待另一个事务解除共享锁才能升级为独占锁,这就造成了死锁。
更新锁有如下特征:
加锁与解锁:当一个事务执行update语句时,数据库系统会先为事务分配一把更新锁。当读取数据完毕,执行更新操作时,会把更新锁升级为独占锁。
兼容性:更新锁与共享锁是兼容的,也就是说,一个资源可以同时放置更新锁和共享锁,但是最多放置一把更新锁。这样,当多个事务更新相同的数据时,只有一个事务能获得更新锁,然后再把更新锁升级为独占锁,其他事务必须等到前一个事务结束后,才能获取得更新锁,这就避免了死锁。
并发性能:允许多个事务同时读锁定的资源,但不允许其他事务修改它。
oracle如何解决锁表问题
Oracle在日常开发过程中,或者业务上线使用过程中,我们会经常遇到锁表问题,导致某一个业务崩溃。这是因为当多个用户同时操作一个表时,或者同一条数据时,很容易发生锁表的情况。这是,由于Oracle数据库为了保持数据的一致性,当某一个用户正在操作一条数据时,若忘记提交,另外一个用户又要对其进行修改时。由于上个操作未提交,导致下一个修改操作一直处于等待状态,当时间长了,就会导致锁表情况的发生。
当我们出现锁表时,新的修改事务发生时,会报下面的错误:
record is locked by another user;
或是出现某一个修改业务长期处于卡死状态,可以考虑是出现锁表的可能性。可以通过数据字典v s e s s i o n 、 v session、v session、vlocked_object等进行关联查询出正处于一直在执行的SQL的sessionID和对应的SQL语句。代码如下:
查看锁表进程SQL语句1:
select sess.sid,sess.serial#, lo.oracle_username,lo.os_user_name, ao.object_name, lo.locked_mode from v$locked_object lo, dba_objects ao, v$session sesswhere ao.object_id = lo.object_id and lo.session_id = sess.sid;
查看锁表进程SQL语句2:
select * from v$session t1, v$locked_object t2 where t1.sid = t2.SESSION_ID;
然后,通过查询出来锁表的SID和serial#对对应的session进行kill(杀死)。代码如下:
alter system kill session 'SID,serial#';
多线程并发之读写锁(ReentranReadWriteLock&ReadWriteLock)使用详解
【1】基本讲解与使用
① ReadWriteLock同Lock一样也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个是只读的锁,一个是写锁。
读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的(排他的)。 每次只能有一个写线程,但是可以有多个线程并发地读数据。
所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
理论上,读写锁比互斥锁允许对于共享数据更大程度的并发。与互斥锁相比,读写锁是否能够提高性能取决于读写数据的频率、读取和写入操作的持续时间、以及读线程和写线程之间的竞争。
② 使用场景
假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。
例如,最初填充有数据,然后很少修改的集合,同时频繁搜索(例如某种目录)是使用读写锁的理想候选项。
在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写。这就需要一个读/写锁来解决这个问题。
③ 互斥原则:
读-读能共存,
读-写不能共存,
写-写不能共存。
④ ReadWriteLock 接口源码示例
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*/
Lock writeLock();
}
其实现类如下:
⑤ 使用示例
实例代码如下:
public class TestReadWriteLock {
public static void main(String[] args){
ReadWriteLockDemo rwd = new ReadWriteLockDemo();
//启动100个读线程
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
rwd.get();
}
}).start();
}
//写线程
new Thread(new Runnable() {
@Override
public void run() {
rwd.set((int)(Math.random()*101));
}
},"Write").start();
}
}
class ReadWriteLockDemo{
//模拟共享资源--Number
private int number = 0;
// 实际实现类--ReentrantReadWriteLock,默认非公平模式
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//读
public void get(){
//使用读锁
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" : "+number);
}finally {
readWriteLock.readLock().unlock();
}
}
//写
public void set(int number){
readWriteLock.writeLock().lock();
try {
this.number = number;
System.out.println(Thread.currentThread().getName()+" : "+number);
}finally {
readWriteLock.writeLock().unlock();
}
}
}
测试结果如下图:
首先启动读线程,此时number为0;然后某个时刻写线程修改了共享资源number数据,读线程再次读取最新值!
【2】ReentrantReadWriteLock源码分析
ReentrantReadWriteLock是ReadWriteLock接口的实现类–可重入的读写锁。
① ReentrantReadWriteLock拥有的特性
1.1获取顺序(公平和非公平)
ReentrantReadWriteLock不会为锁定访问强加读或者写偏向顺序,但是它确实是支持可选的公平策略。
非公平模式(默认)
构造为非公平策略(缺省值)时,读写锁的入口顺序未指定,这取决于可重入性约束。持续竞争的非公平锁可以无限期地延迟一个或多个读写器线程,但通常具有比公平锁更高的吞吐量。
公平模式
当构造为公平策略时,线程使用近似的到达顺序策略(队列策略)争夺输入。当释放当前持有的锁时,要么最长等待的单个写入线程将被分配写锁,或者如果有一组读取线程等待的时间比所有等待的写入线程都长,那么该组读线程组将被分配读锁。
如果写入锁被占有,或者存在等待写入线程,则试图获取公平读取锁(非可重入)的线程将阻塞。直到当前等待写入线程中最老的线程获取并释放写入锁之后,该线程才会获取读取锁。当然,如果等待的写入线程放弃等待,剩下一个或多个读取器线程作为队列中最长的等待器而没有写锁,那么这些读取器将被分配读锁。
试图获得公平写锁(非可重入)的线程将阻塞,除非读锁和写锁都是空闲的(这意味着没有等待的线程)。(请注意,非阻塞 ReadLock#tryLock()和{@link WriteLock#tryLock()方法不遵守此公平策略设置,并且如果可能的话将立即获取锁,而不管等待的线程)。构造器源码如下:
//默认非公平模式
public ReentrantReadWriteLock() {
this(false);
}
//使用给定的策略创建ReentrantReadWriteLock,true--公平 false-nonfair
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
可以看到,默认的构造方法使用的是非公平模式,创建的Sync是NonfairSync对象,然后初始化读锁和写锁。一旦初始化后,ReadWriteLock接口中的两个方法就有返回值了,如下:
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
1.2可重入
这个锁允许读线程和写线程以ReentrantLock的语法重新获取读写锁。在写入线程保持的所有写入锁被释放之前,不允许不可重入的读线程。
另外,写锁(写线程)可以获取读锁,但是不允许读锁(读线程)获取写锁。在其他应用程序中,当对在读锁下执行读取的方法或回调期间保持写锁时,可重入性可能非常有用。
1.3锁降级
可重入特性还允许从写锁降级到读锁—通过获取写锁,然后获取读锁,然后释放写锁。但是,从读锁到写锁的升级是不可能的。
1.4锁获取的中断
在读锁和写锁的获取过程中支持中断 。
1.5支持Condition
Condition详解参考:Condition与Lock使用详解。
写锁提供了Condition实现,ReentrantLock.newCondition;读锁不支持Condition。
1.6监控
该类支持确定锁是否持有或争用的方法。这些方法是为了监视系统状态而设计的,而不是用于同步控制。
② 特性使用实例
2.1锁降级
class CachedData {
Object data;
//volatile修饰,保持内存可见性
volatile boolean cacheValid;
//可重入读写锁
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
//首先获取读锁
rwl.readLock().lock();
//发现没有缓存数据则放弃读锁,获取写锁
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
//进行锁降级
rwl.writeLock().unlock();
// Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}}
2.2集合使用场景
通常可以在集合使用场景中看到ReentrantReadWriteLock的身影。不过只有在集合比较大,读操作比写操作多,操作开销大于同步开销的时候才是值得的。
实例如下:
class RWDictionary {
//集合对象
private final Map<String, Data> m = new TreeMap<String, Data>();
//读写锁
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
//获取读锁
private final Lock r = rwl.readLock();
//获取写锁
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}}
【3】Sync、FairSync和NonfairSync
再次回顾ReentrantReadWriteLock构造方法:
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
从上面可以看到,构造方法决定了Sync是FairSync还是NonfairSync。Sync继承了AbstractQueuedSynchronizer,而Sync是一个抽象类,NonfairSync和FairSync继承了Sync,并重写了其中的抽象方法。参考博文:队列同步器AQS-AbstractQueuedSynchronizer 原理分析。
① Sync的两个抽象方法
Sync中提供了很多方法,但是有两个方法是抽象的,子类必须实现。
// 如果当前线程在试图获取读取锁时由于线程等待策略而应该阻塞,则返回true。
abstract boolean readerShouldBlock();
//如果当前线程在试图获取写锁时由于线程等待策略而应该阻塞,则返回true。
abstract boolean writerShouldBlock();
writerShouldBlock和readerShouldBlock方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞。
② FairSync类:
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
对于公平模式,hasQueuedPredecessors()方法表示前面是否有等待线程。一旦前面有等待线程,那么为了遵循公平,当前线程也就应该被挂起。
③ NonfairSync 类:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}
从上面可以看到,非公平模式下,writerShouldBlock直接返回false,说明不需要阻塞,可以直接获取锁。
而readShouldBlock调用了apparentFirstQueuedIsExcluisve()方法。该方法在当前线程是写锁占用的线程时,返回true,否则返回false。即,如果当前有一个写线程正在写,那么该读线程应该阻塞。
④ Sync的几个成员变量和静态内部类
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/
static final int SHARED_SHIFT = 16;//共享移位 16
static final int SHARED_UNIT = (1 << SHARED_SHIFT);//65536
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;//65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//65535
//返回shared保持的计数-无符号右移
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
//返回exclusive 保持的计数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
//用于每线程读取保持计数的计数器。作为一个ThreadLocal,缓存在cachedHoldCounter
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
/**
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;
private transient HoldCounter cachedHoldCounter;
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
⑤ 核心方法tryReadLock
//为读取执行tryLock,在两种模式下"驳船"行为都可用。这实际上与tryAcquireShared相同,只是没有调用readerShouldBlock。
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
//如果有写锁占用,返回false
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);
//判断是否达到最大值
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//CAS算法更新
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return true;
}
}
}
⑥ 核心方法tryWriteLock
//为写入执行tryLock,在两种模式下"驳船"行为都可用。这实际上与tryAcquire 相同,只是没有调用writerShouldBlock。
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
//获取持有计数
int w = exclusiveCount(c);
// 0 表示没有获取到锁
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//已经达到最大值
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
//CAS出了问题
if (!compareAndSetState(c, c + 1))
return false;
//设置当前线程为拥有独占访问权的线程
setExclusiveOwnerThread(current);//AQS的setExclusiveOwnerThread
return true;
}
【4】ReadLock
读锁源码实例如下:
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
//如果写锁没有被其他线程获取则获取读锁并立即返回。
// 如果写入锁由另一个线程持有,则当前线程出于线程调度目的而禁用,并处于休眠状态,直到获取了读锁。
public void lock() {
sync.acquireShared(1);//共享模式 使用AQS的acquireShared方法
}
//获取读锁除非当前线程被中断;
//如果写锁没有被其他线程获取则获取读锁并立即返回。
// 如果写入锁由另一个线程持有,则当前线程出于线程调度目的而禁用,并处于休眠状态,直到获取了读锁。
//如果读锁被当前线程获取,或者被别的线程中断了当前线程。在上述情况下,如果当前线程 在进入该方法时已经设置了中断状态或
//者在获取读锁时被中断,则抛出InterruptedException并将当前线程的中断状态清除。
//在该实现中,由于该方法是显式的中断点,因此相对于锁的正常或可重入获取,优先考虑对中断作出响应。
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);//同样调用AQS的acquireSharedInterruptibly
}
//在调用时写入锁不被另一个线程持有时才获取读锁,并立即返回true。
//即使将此锁设置为使用公平排序策略,如果锁可用,则调用{tryLock()}将立即获取读取锁,而不管其他线程当前是否正在等待读取锁。
//这种“驳船行为”在某些情况下是有用的,即使它破坏了公平性。
//如果希望遵守此锁的公平性设置,则使用{@link#tryLock(long,TimeUnit)tryLock(0,TimeUnit.SECONDS)}这几乎等效(它还检测中断)。
//如果写锁被其他线程获取,则立即返回false。
public boolean tryLock() {
return sync.tryReadLock();//Sync的tryReadLock方法
}
//如果写入锁在给定的等待时间内没有被其他线程持有,并且当前线程没有被中断,则获取读取锁。
//如果写锁没有被其他线程持有则获取读锁并返回true。
//如果该锁已被设置为使用公平排序策略,那么如果任何其他线程正在等待该锁,则当前线程不会获取可用的读锁。
//这与tryLock方法形成对比。
//如果想要一个Timed的tryLock在一个公平锁上,你可以使用如下两种方式结合在一起:
* if (lock.tryLock() ||
* lock.tryLock(timeout, unit)) {
* ...
* }}
//如果写锁被其他线程持有则当前线程由于线程调度目的进入睡眠直到下面事情之一发生:
* 读锁被当前线程获取;
* 别的线程中断了当前线程
* 指定的等待时间过去。
//如果获取了读锁则返回true。
//如果当前线程 在进入该方法时已经设置了中断状态或者在获取读锁时被中断,则抛出InterruptedException并将当前线程的中断状态清除。
//如果指定等待时间过去则返回false。如果时间小于或等于零,则该方法根本不会等待。
//在该实现中,由于该方法是显式的中断点,因此优先考虑响应中断,而不是正常或可重入地获取锁及报告等待时间的流逝。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));//AQS的tryAcquireSharedNanos
}
//试图释放锁,如果读取器的数量现在是零,那么写锁可以获取。
public void unlock() {
sync.releaseShared(1);//AQS的releaseShared
}
//ReadLocks不支持Condition ,会抛出UnsupportedOperationException
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
【5】WriteLock
WriteLock 源码实例如下:
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
//如果读锁和写锁均未被其他线程持有,则获取写锁并设置写锁计数为one。
//如果当前线程已经持有写锁,则保持计数递增1,并且方法立即返回。
//如果锁由另一个线程持有,则当前线程出于线程调度目的而禁用,并处于休眠状态,
//直到获取了写锁,此时将写锁保持计数设置为one。
public void lock() {
sync.acquire(1);//这里使用AQS的acquire方法
}
//获取写锁除非当前线程被中断。
//如果读锁和写锁均未被其他线程持有,则获取写锁并设置写锁计数为one。
//如果当前线程已经持有写锁,则保持计数递增1,并且方法立即返回。
如果锁由另一个线程持有,则当前线程出于线程调度目的而禁用,并处于休眠状态,直到下面事情发生:
* 当前线程获取了写锁;
* 别的线程中断了当前线程
//如果当前线程在进入这个方法时已经设置了它的中断状态或者当获取写锁时被中断则抛出InterruptedException并清空中断状态
//在该实现中,由于该方法是显式的中断点,因此相对于锁的正常或可重入获取,优先考虑对中断作出响应。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);//AQS的acquireInterruptibly
}
//如果没有被其他线程在调用该方法时获取写锁,则当前线程获取写锁。
//如果没有读锁或者写锁被其他线程持有,则当前线程获取写锁并返回true且设置写锁持有计数为one。
//即使锁被设置了公平策略,调用tryLock()方法将会立即获取锁(如果锁可用),不管当前是否有其他线程在等待写锁。
//这种“驳船行为”在某些情况下是有用的,即使它破坏了公平性。
//如果想保持锁的公平性,则尝试使用tryLock(long, TimeUnit) tryLock(0, TimeUnit.SECONDS) 二者是等效的。
//如果当前线程已经持有了锁,则将持有计数+1并返回true。
//如果锁被其他线程持有则立即返回false。
public boolean tryLock( ) {
return sync.tryWriteLock();//Sync的tryWriteLock
}
//获取写锁,如果锁没有被其他线程持有在等待时间内并且当前线程没有被中断。
//如果没有读锁或者写锁被其他线程持有则获取写锁并且返回true,同时将写锁持有计数设置为one。
//如果锁被设置了公平策略,则可能获取不到可用的锁如果有其他的线程在等待这个写锁。
//这与tryLock()方法形成了对比。
//如果想要一个定时的tryLock可以使用如下方式结合:
* if (lock.tryLock() ||
* lock.tryLock(timeout, unit)) {
* ...
* }}</pre>
//如果当前线程已经持有锁,则将持久计数+1,然后返回true。
//如果锁被其他线程持有,则出于线程调度目的当前线程将会进入睡眠状态直到以下三件事情发生之一:
* 当前线程获取写锁;
* 别的线程中断了当前线程;
* 指定等待时间过去。
//如果当前线程获取到写锁则返回true并将写锁持有计数设置为one。
//如果当前线程在进入该方法时设置了中断状态或者在尝试获取写锁时被中断,则会抛出InterruptedException并将中断状态清空。
//如果指定等待时间过去,则返回false。如果time小于或者等于0,方法不会再等待。
//在该实现中,由于该方法是显式的中断点,因此优先考虑响应中断,而不是正常或可重入地获取锁及报告等待时间的流逝 。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));//AQS的tryAcquireNanos
}
//试图释放锁。如果当前线程是锁的持有者,则将持有计数减一。如果持有计数为0,则释放锁。
//如果当前线程不是锁的持有者,则抛出IllegalMonitorStateException。
public void unlock() {
sync.release(1);//AQS的release
}
//返回Lock实例使用的Condition实例-写锁支持,读锁不支持Condition。
//Condition实例支持类似于Object监视器方法如wait/notify/notifyAll的监视器方法,如Condition.await(),signal(),signalAll().
//当调用Condition方法但是写锁没有被持有则抛出IllegalMonitorStateException。
//(读取锁独立于写入锁保存,因此不会被检查或受到影响。然而,在当前线程还获取了读锁时,
//调用条件等待方法本质上总是一个错误,因为其他可能解锁的线程将无法获取写锁。)
//当Condition#await()方法被调用时,写锁被释放,并且在它们返回之前,重新获取写锁,并且锁保持计数恢复到调用方法时的状态。
//当一个线程在等待的时候被中断,则抛出InterruptedException并且将该线程的中断状态清空。
//等待线程以FIFO顺序被唤醒。
//对于从等待方法返回的线程,重新获取锁的顺序与最初获取锁的线程相同,在默认情况下,没有指定锁,
//但是对于公平锁,那些等待时间最长的线程更有利。
public Condition newCondition() {
return sync.newCondition();
}
public String toString() {
Thread o = sync.getOwner();
return super.toString() + ((o == null) ?
"[Unlocked]" :
"[Locked by thread " + o.getName() + "]");
}
//查询写锁是否被当前线程持有
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();//Sync的isHeldExclusively
}
//查询当前线程对这个写锁的保持次数。线程对每个未与解锁操作匹配的锁操作都持有锁
public int getHoldCount() {
return sync.getWriteHoldCount();//Sync的getWriteHoldCount
}
}
ReentrantReadWriteLock.Sync.getWriteHoldCount源码如下:
final int getWriteHoldCount() {
return isHeldExclusively() ? exclusiveCount(getState()) : 0;
}
waiting…
有了事务为什么还需要乐观锁和悲观锁
事务是粗粒度的概念、乐观锁悲观锁可以更细粒度的控制;
比如抢票,假设余票只有1张;隔离级别可以保证事务A和事务B不能读到对方的数据,也不能更新对方正在更新的数据,但是事务A和事务B都认为还有1张余票,于是出票,并更新为0;
事务解决了并发问题,已经不存在并发问题了;
但是事务B读取的是过时数据,依据过时数据做了业务处理;
所以需要乐观锁或者悲观锁,来记录一个信息:当前已经读取的数据,是不是已经过时了!
事物的核心就是锁+并发
事务有这么几种实现方式:锁协议、MVCC、时间戳排序协议、有效性检查协议,锁协议是事务的一种实现方式,
事务 = 用锁封装的一个函数,可以重用而已,但是这几个事务的函数覆盖面太粗粒度了,所以有时候我们还得借助于锁来进行细粒度控制;
事务不能保证每个操作结果正确,售票时超卖还是会发生。
事务保证整个操作的成一个组,要么全做要么全不做 但是不能保证多个事务同时读取同一个数据
数据对象被加上排它锁时,其他的事务不能对它读取和修改;加了共享锁的数据对象可以被其他事务读取,但不能修改
事务可以用锁实现,可以保证一致性和隔离性,但是锁用来保证并发性;
隔离性和并发性有点类似,但是隔离性只是保证不会出现相互读取中间数据,却无法解决并发的问题
参考借鉴blog: https://blog.csdn.net/lsziri/article/details/80656600
https://blog.csdn.net/csdn_huzeliang/article/details/78995795
https://blog.csdn.net/turodog/article/details/87868994
https://blog.csdn.net/weixin_39625809/article/details/80707695
https://www.cnblogs.com/xrq730/p/5087378.html
https://blog.csdn.net/weixin_38070406/article/details/78157603
https://www.jianshu.com/p/2db919fe19b9
https://www.jianshu.com/p/2b113ca4bfa1
https://blog.csdn.net/C_J33/article/details/79487941
https://blog.csdn.net/j080624/article/details/82790372