Transactional,ReentrantLock,@Resource

微信里收藏了不少技术文章,一直没时间看,这段时间看一下,对一些要点进行摘录,后期可以翻阅

事务失效的6种情况

1.方法自调用
声明式事务底层其实就是 AOP,所以在声明式事务中,我们拿到的服务类并不是服务类本身,而是一个代理对象,在这个代理对象中的代理方法中,自动添加了事务的逻辑,所以如果我们直接方法自调用,没有经过这个代理对象,事务就会失效。

public class UserService{
   @Transactional
   public void sayHello(){}
}

如果我们在 UserController 中注入 UserService,那么拿到的并不是 UserService 对象本身,而是通过动态代理为 UserService 生成的一个动态代理类,这个动态代理就类似下面这样(伪代码):

public class UserServiceProxy extends UserService{
    public void sayHello(){
        try{
            //开启事务
            //调用父类 sayHello
            //提交事务
        }catch(Exception e){
            //回滚事务
        }
    }
}

所以你最终调用的并不是 UserService 本身的方法,而是动态代理对象中的方法。因此,如果存在这样的代码:

public class UserService{
   @Transactional
   public void sayHello(){}
   public void useSayHello(){sayHello();}
}

在 useSayHello 中调用 sayHello 方法,sayHello 方法上虽然有事务注解,但是这里的事务不生效(因为调用的不是的动态代理对象中的 sayHello 方法,而是当前对象 this 的 sayHello 方法)。
2.异常被捕获
如果我们在 sayHello 方法中将异常捕获了,那么动态代理类中的方法,就感知到目标方法发生异常了,自然也就不会自动处理事务回滚了。还是以前面的 UserServiceProxy 为例:

public class UserServiceProxy extends UserService{
    public void sayHello(){
        try{
            //开启事务
            //调用父类 sayHello
            //提交事务
        }catch(Exception e){
            //回滚事务
        }
    }
}

如果调用父类 sayHello 的时候,sayHello 方法自动将异常捕获了,那么很明显,这里就不会进行异常回滚了
3.方法非 public
4.非运行时异常:默认情况下,只会捕获 RuntimeException
5.不是 Spring Bean
声明式事务主要是通过动态代理来处理事务的,如果你拿到手的 UserService 对象就是原原本本的 UserService(如果自己 new 了一个 UserService 就是这种情况),那么事务代码在哪里?没有事务处理的代码,事务自然不会生效。

声明式事务的核心,就是动态代理生成的那个对象,没有用到那个对象,事务就没戏。

6.数据库不支持事务
长事务:
@Transactional 注解,是使用 AOP 实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。
当 Spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,对于@Transactional注解包裹的整个方法都是使用同一个connection连接。如果我们出现了耗时的操作,比如第三方接口调用,业务逻辑复杂,大批量数据处理等就会导致我们我们占用这个connection的时间会很长,数据库连接一直被占用不释放。一旦类似操作过多,就会导致数据库连接池耗尽。
在一个事务中执行RPC操作导致数据库连接池撑爆属于是典型的长事务问题,类似的操作还有在事务中进行大量数据查询,业务规则处理等…

如何避免长事务?
解决长事务的宗旨就是 对事务方法进行拆分,尽量让事务变小,变快,减小事务的颗粒度。
声明式事务:通过在方法上使用@Transactional注解进行事务管理的操作叫声明式事务 。缺点,就是事务的颗粒度是整个方法,无法进行精细化控制。
编程式事务:基于底层的API,开发者在代码中手动的管理事务的开启、提交、回滚等操作。在spring项目中可以使用TransactionTemplate类的对象,手动控制事务。

@Autowired 
private TransactionTemplate transactionTemplate; 
 
... 

public void save(RequestBill requestBill) { 
    transactionTemplate.execute(transactionStatus -> {
        requestBillDao.save(requestBill);
        //保存明细表
        requestDetailDao.save(requestBill.getDetail());
        return Boolean.TRUE; 
    });
}

避免长事务最简单的方法就是不要使用声明式事务@Transactional,而是使用编程式事务手动控制事务范围。
如果使用@Transactional,需要对方法进行拆分,将不需要事务管理的逻辑与事务操作分开:

@Service
public class OrderService{

    public void createOrder(OrderCreateDTO createDTO){
        query();
        validate();
        saveData(createDTO);
    }
  
  //事务操作
    @Transactional(rollbackFor = Throwable.class)
    public void saveData(OrderCreateDTO createDTO){
        orderDao.insert(createDTO);
    }
}

query()与validate()不需要事务,我们将其与事务方法saveData()拆开。这种拆分会命中使用@Transactional注解时事务不生效的经典场景,很多新手非常容易犯这个错误。@Transactional注解的声明式事务是通过spring aop起作用的,而spring aop需要生成代理对象,直接在同一个类中方法调用使用的还是原始对象,事务不生效
正确的拆分方法应该使用下面两种
1.可以将方法放入另一个类,如新增 manager层,通过spring注入,这样符合了在对象之间调用的条件。

@Service
public class OrderService{
    @Autowired
   private OrderManager orderManager;

    public void createOrder(OrderCreateDTO createDTO){
        query();
        validate();
        orderManager.saveData(createDTO);
    }
}

@Service
public class OrderManager{
  
    @Autowired
   private OrderDao orderDao;
  
  @Transactional(rollbackFor = Throwable.class)
    public void saveData(OrderCreateDTO createDTO){
        orderDao.saveData(createDTO);
    }
}

2.启动类添加@EnableAspectJAutoProxy(exposeProxy = true),方法内使用AopContext.currentProxy()获得代理类,使用事务。

SpringBootApplication.java

@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {}
OrderService.java
  
public void createOrder(OrderCreateDTO createDTO){
    OrderService orderService = (OrderService)AopContext.currentProxy();
    orderService.saveData(createDTO);
}

ReentrantLock

实现原理:volatile 变量 + CAS设置值 + AQS + 两个队列

实现阻塞:同步队列 + CAS抢占标记为 valatile 的 state

实现等待唤醒:await :持有锁,park ->加入等待队列 ;signal:唤醒下一个等待队列节点,转移进入同步队列,然后CAS抢占或者按照阻塞队列等待抢占。接着 await 后续内容程序得以继续执行。

ReentrantLock 继承了Lock接口, lock方法实际上是调用了Sync的子类NonfairSync(非公平锁)的lock方法。ReentrantLock的真正实现在他的两个内部类NonfairSync 和 FairSync中,默认实现是非公平锁。并且内部类都继承于内部类Sync,而Sync根本的实现则是大名鼎鼎的 AbstractQueuedSynchronizer 同步器(AQS)。
与Synchronized相同点:
1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。

但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。

2.ReentrantLock和synchronized都是可重入锁。

synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

3.都可以实现线程之间的等待通知机制。使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

与Synchronized 不同点:
1.ReentrantLock是Java层面的实现,synchronized是JVM层面的实现。
2.使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),而ReentrantLock需要手动释放锁需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。
3.synchronized是非公平锁,ReentrantLock可以实现公平和非公平锁。
4.ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。配合重试机制更好的解决死锁。
5.ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
6.ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。
7.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁。

为什么 Spring 和 IDEA 都不推荐使用 @Autowired 注解

使用IDEA开发的时候,在字段上使用Spring的依赖注入注解@Autowired后会出现如下警告

Field injection is not recommended (字段注入是不被推荐的)

Spring常见的DI方式

构造器注入:利用构造方法的参数注入依赖
Setter注入:调用Setter的方法注入依赖
字段注入:在字段上使用@Autowired/Resource注解

@Autowired VS @Resource
都是通过注解实现依赖注入,只不过@Autowired是Spring定义的,而@Resource是JSR-250定义的。大致功能基本相同,但是还有一些细节不同:

依赖识别方式:@Autowired默认是byType可以使用@Qualifier指定Name,@Resource默认ByName如果找不到则ByType
适用对象:@Autowired可以对构造器、方法、参数、字段使用,@Resource只能对方法、字段使用
提供方:@Autowired是Spring提供的,@Resource是JSR-250提供的

各种DI方式的优缺点
参考Spring官方文档,建议了如下的使用场景:

构造器注入:强依赖性(即必须使用此依赖),不变性(各依赖不会经常变动)
Setter注入:可选(没有此依赖也可以工作),可变(依赖会经常变动)
Field注入:大多数情况下尽量少使用字段注入,一定要使用的话, @Resource相对@Autowired对IoC容器的耦合更低

Field注入的缺点

不能像构造器那样注入不可变的对象
依赖对外部不可见,外界可以看到构造器和setter,但无法看到私有字段,自然无法了解所需依赖
会导致组件与IoC容器紧耦合(这是最重要的原因,离开了IoC容器去使用组件,在注入依赖时就会十分困难)
导致单元测试也必须使用IoC容器,原因同上
依赖过多时不够明显,比如我需要10个依赖,用构造器注入就会显得庞大,这时候应该考虑一下此组件是不是违反了单一职责原则

优点就是方便,使用构造器或者setter注入需要写更多业务无关的代码,十分麻烦,而字段注入大幅简化了它们。并且绝大多数情况下业务代码和框架就是强绑定的,完全松耦合只是一件理想上的事,牺牲了敏捷度去过度追求松耦合反而得不偿失。

为什么IDEA只对@Autowired警告
@Autowired是Spring提供的,它是特定IoC提供的特定注解,这就导致了应用与框架的强绑定,一旦换用了其他的IoC框架,是不能够支持注入的。而 @Resource是JSR-250提供的,它是Java标准,我们使用的IoC容器应当去兼容它,这样即使更换容器,也可以正常工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值