Spring事务切面_传播属性(8)

目录

1. 传播属性

2. 案例分享

2.1 测试说明

2.2  Propagation.REQUIRED 演示

案例1:

案例2:

案例3:

案例4:

总结1:

案例5:

案例6: 特殊的传播属性 NESTED 错误使用

案例6的解决方案:

3. 带着问题看源码

3.1 类分析:

3.2 源码分析

3.2.1 事务创建

 3.2.2 事务的链式调用

源码case:

3.2.3 事务的回滚。

总结2:

3.2.4. 伪代码分析:


1. 传播属性

Spring特有一套处理事务处理的逻辑,而今天要讲的传播属性就是Spring独有的。下面对传播属性进行介绍:

PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。

PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

以上传播属性,使用最高频的是: PROPAGATION_REQUIREDPROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED。其实,所谓的传播属性,就是控制Connection对数据库进行操作的。传统的JDBC连接数据库如下:

 Connection connection = null;
        try {
            connection = ConnectionUtil.getConnection();
            //开启事务
            /*
            *
            * */
            connection.setAutoCommit(false);
            insertTest(connection);
            insertTest1(connection);


            connection.commit();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
            try {
                connection.rollback();
                System.out.println("JDBC Transaction rolled back successfully");
            } catch (SQLException e1) {
                System.out.println("JDBC Transaction rolled back fail" + e1.getMessage());
            }
        } finally {
            if (connection != null) {
                try {
                    selectAll(connection);
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

也就是说,每一次连接数据库进行操作,我们底层都需要生成Connection对象,通过Connection对象实际上进行数据库的连接操作。而在我们Spring的事务中,就是按照Spring的逻辑对Connection进行不同的封装调用而已。具体点说就是按照你在业务代码中配置的事务传播属性,进行不同逻辑的Connection生成和封装对象,然后进行数据库的连接。

2. 案例分享

2.1 测试说明

        首先,我们需要一个测试类和一个假设的业务类,但是这个业务类调用的是其他业务类的方法,他们都带有事务。

        测试类:我们会执行propagationTest方法:

package com.xuexi.jack.test;

import com.xuexi.jack.bean.ComponentScanBean;
import com.xuexi.jack.pojo.ConsultConfigArea;
import com.xuexi.jack.pojo.ZgGoods;
import com.xuexi.jack.service.AccountService;
import com.xuexi.jack.service.area.AreaService;
import com.xuexi.jack.service.transaction.TransationService;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.HashMap;
import java.util.Map;

public class TransactionTest {

    private ApplicationContext applicationContext;

    @Before
    public void before() {
        applicationContext = new AnnotationConfigApplicationContext(ComponentScanBean.class);
    }

    @Test
    public void test1() {
        AreaService bean = applicationContext.getBean(AreaService.class);
        Map param = new HashMap();
        param.put("areaCode","1001");
        bean.queryAreaFromDB(param);
    }

    @Test
    public void addAreaTest() {
        AreaService bean = applicationContext.getBean(AreaService.class);
        ConsultConfigArea area = new ConsultConfigArea();
        area.setAreaCode("VV1");
        area.setAreaName("VV1");
        area.setState("1");
        bean.addArea(area);
    }

    @Test
    public void propagationTest() {
        String areaStr = "HN1";
        String goodsStr = "iphone 2";
        TransationService transationService = applicationContext.getBean(TransationService.class);
        ConsultConfigArea area = new ConsultConfigArea();
        area.setAreaCode(areaStr);
        area.setAreaName(areaStr);
        area.setState("1");

        ZgGoods zgGoods = new ZgGoods();
        zgGoods.setGoodCode(goodsStr);
        zgGoods.setGoodName(goodsStr);
        zgGoods.setCount(100);
        transationService.transation(area,zgGoods);
    }

    @Test
    public void accountServiceTest() {
        AccountService bean = applicationContext.getBean(AccountService.class);
        bean.queryAccount("d");
    }

}

中转业务类,关注 transation 方法:

package com.xuexi.jack.service.transaction;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import com.xuexi.jack.pojo.ZgGoods;
import com.xuexi.jack.pojo.ZgTicket;
import com.xuexi.jack.service.area.AreaService;
import com.xuexi.jack.service.goods.GoodsService;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service("transationServiceImpl")
public class TransationServiceImpl implements TransationService {

    @Autowired
    AreaService areaService;

    @Autowired
    GoodsService goodsService;

    @Autowired
    CommonMapper commonMapper;

    //开启了事务
    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void transation(ConsultConfigArea area, ZgGoods zgGoods) {
        //try {
            areaService.addArea( area);
            goodsService.addGoods(zgGoods);
       /* }catch (Exception e) {

        }*/
    }
    //提交事务


    @Transactional
    @Override
    public int getTicket() {

        //1、获取锁
        List<ZgTicket> zgTickets = commonMapper.queryTicketById("12306");
        Map lockmap = new HashMap();
        lockmap.put("ticketId", "12306");
        lockmap.put("version", zgTickets.get(0).getVersion());
        int i = commonMapper.updateLock(lockmap);

        if (i > 0) {
            //抢票
            ZgTicket zgTicket = zgTickets.get(0);
            zgTicket.setTicketCount(2);
            int i1 = commonMapper.updateTicket(zgTicket);
        } else {
            //继续抢
            ((TransationService) AopContext.currentProxy()).getTicket();
        }

        return 0;
    }

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Override
    public int getTicketModeOne() {

        Integer execute = transactionTemplate.execute(status -> {
            //1、获取锁
            List<ZgTicket> zgTickets = commonMapper.queryTicketById("12306");
            Map lockmap = new HashMap();
            lockmap.put("ticketId", "12306");
            lockmap.put("version", zgTickets.get(0).getVersion());
            int i = commonMapper.updateLock(lockmap);

            if (i > 0) {
                //抢票
                ZgTicket zgTicket = zgTickets.get(0);
                zgTicket.setTicketCount(2);
                int i1 = commonMapper.updateTicket(zgTicket);
            }
            return i;
        });

        if (execute == 0) {
            //继续抢
            getTicketModeOne();
        }
        return 0;
    }
}

 在上方的业务中转类中,我们设置了事务隔离属性为 propagation = Propagation.REQUIRED,并且它还按顺序,调用了另外2个类。下面是各种case分享:

2.2  Propagation.REQUIRED 演示

案例1:

 areaService.addArea( area) 和 goodsService.addGoods(zgGoods)方法的事务传播属性都是 REQUIRED,但是在addArea方法中抛异常。此时,2张表都无法插入数据。因为他们使用的是同一个Connection对象连接的数据库,而addArea方法抛异常,导致后面的方法无法被执行到。同时,因为主方法 transation 也有事务,因此他会返回到主方法处进行回滚。所以2张表都没有数据。

AreaServiceImpl:
package com.xuexi.jack.service.area;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

//@PropertySource("classpath:config/core/core.properties")
@Service
public class AreaServiceImpl implements AreaService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private CommonMapper commonMapper;

    @Autowired
    AreaService areaService;

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public int addArea(ConsultConfigArea area) {
        int i = commonMapper.addArea(area);
        if(true) throw new RuntimeException("yic");
     /*   try {
            if (true) {throw new RuntimeException("111");}
        }
        catch (Exception e){}*/

        return 1;
    }
}
GoodsServiceImpl:
    package com.xuexi.jack.service.goods;
    
    import com.xuexi.jack.dao.CommonMapper;
    import com.xuexi.jack.pojo.ZgGoods;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    
    @Service
    public class GoodsServiceImpl implements GoodsService {
    
        @Autowired
        CommonMapper commonMapper;
    
        @Transactional(propagation = Propagation.REQUIRED)
        @Override
        public void addGoods(ZgGoods zgGoods) {
            int i = commonMapper.addGood(zgGoods);
            //if(true) throw new RuntimeException("yic");
           /* try {
                if(true) throw new RuntimeException("yic");
            }catch (Exception e) { }*/
    
        }
    
        @Transactional(readOnly = true)
        @Override
        public List<ZgGoods> queryAll() {
            return commonMapper.queryAll();
        }
    }

案例2:

 areaService.addArea( area) 和 goodsService.addGoods(zgGoods)方法的事务传播属性都是 REQUIRED,但是在addArea方法中有异常并且自己捕获到了异常,不再往上抛异常。goodsService.addGoods(zgGoods)方法保持不变,此时执行测试方法,我们会发现,2张表都有数据。原因是异常被我们自己写的 try...catch 捕获并吞掉, spring并没有获取到异常信息。因此2张表会正常的插入数据

AreaServiceImpl:
package com.xuexi.jack.service.area;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

//@PropertySource("classpath:config/core/core.properties")
@Service
public class AreaServiceImpl implements AreaService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private CommonMapper commonMapper;

    @Autowired
    AreaService areaService;

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public int addArea(ConsultConfigArea area) {
        int i = commonMapper.addArea(area);
        //if(true) throw new RuntimeException("yic");
        try {
            if (true) {throw new RuntimeException("111");}
        }
        catch (Exception e){}

        return 1;
    }
}
GoodsServiceImpl:
    package com.xuexi.jack.service.goods;

    import com.xuexi.jack.dao.CommonMapper;
    import com.xuexi.jack.pojo.ZgGoods;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;

    import java.util.List;

    @Service
    public class GoodsServiceImpl implements GoodsService {

        @Autowired
        CommonMapper commonMapper;

        @Transactional(propagation = Propagation.REQUIRED)
        @Override
        public void addGoods(ZgGoods zgGoods) {
            int i = commonMapper.addGood(zgGoods);
            //if(true) throw new RuntimeException("yic");
           /* try {
                if(true) throw new RuntimeException("yic");
            }catch (Exception e) { }*/

        }

        @Transactional(readOnly = true)
        @Override
        public List<ZgGoods> queryAll() {
            return commonMapper.queryAll();
        }
    }

案例3:

 areaService.addArea( area) 和 goodsService.addGoods(zgGoods)方法的事务传播属性都是 REQUIRED,但是在addGoods方法中抛异常。此时,2张表都无法插入数据。因为他们使用的是同一个Connection对象连接的数据库,而addGoods方法抛异常会被主方法 transation 捕获到,因此他会返回到主方法处进行回滚。所以2张表都没有数据。

 AreaServiceImpl

package com.xuexi.jack.service.area;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

//@PropertySource("classpath:config/core/core.properties")
@Service
public class AreaServiceImpl implements AreaService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private CommonMapper commonMapper;

    @Autowired
    AreaService areaService;

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public int addArea(ConsultConfigArea area) {
        int i = commonMapper.addArea(area);
        //if(true) throw new RuntimeException("yic");
      /*  try {
            if (true) {throw new RuntimeException("111");}
        }
        catch (Exception e){}*/

        return 1;
    }
}

GoodsServiceImpl

    package com.xuexi.jack.service.goods;

    import com.xuexi.jack.dao.CommonMapper;
    import com.xuexi.jack.pojo.ZgGoods;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;

    import java.util.List;

    @Service
    public class GoodsServiceImpl implements GoodsService {

        @Autowired
        CommonMapper commonMapper;

        @Transactional(propagation = Propagation.REQUIRED)
        @Override
        public void addGoods(ZgGoods zgGoods) {
            int i = commonMapper.addGood(zgGoods);
            if(true) throw new RuntimeException("yic");
           /* try {
                if(true) throw new RuntimeException("yic");
            }catch (Exception e) { }*/

        }

        @Transactional(readOnly = true)
        @Override
        public List<ZgGoods> queryAll() {
            return commonMapper.queryAll();
        }
    }

案例4:

 areaService.addArea( area) 和 goodsService.addGoods(zgGoods)方法的事务传播属性都是 REQUIRED,但是在addGoods方法中有异常并且自己捕获到了异常,不再往上抛异常。执行测试用来,我们会发现,2张表都有数据。原因是异常被我们自己写的 try...catch 捕获并吞掉, spring并没有获取到异常信息。因此2张表会正常的插入数据

AreaServiceImpl:

package com.xuexi.jack.service.area;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

//@PropertySource("classpath:config/core/core.properties")
@Service
public class AreaServiceImpl implements AreaService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private CommonMapper commonMapper;

    @Autowired
    AreaService areaService;

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public int addArea(ConsultConfigArea area) {
        int i = commonMapper.addArea(area);
        //if(true) throw new RuntimeException("yic");
      /*  try {
            if (true) {throw new RuntimeException("111");}
        }
        catch (Exception e){}*/

        return 1;
    }
}

 GoodsServiceImpl :

    package com.xuexi.jack.service.goods;

    import com.xuexi.jack.dao.CommonMapper;
    import com.xuexi.jack.pojo.ZgGoods;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;

    import java.util.List;

    @Service
    public class GoodsServiceImpl implements GoodsService {

        @Autowired
        CommonMapper commonMapper;

        @Transactional(propagation = Propagation.REQUIRED)
        @Override
        public void addGoods(ZgGoods zgGoods) {
            int i = commonMapper.addGood(zgGoods);
            //if(true) throw new RuntimeException("yic");
           try {
                if(true) throw new RuntimeException("yic");
            }catch (Exception e) { }

        }

        @Transactional(readOnly = true)
        @Override
        public List<ZgGoods> queryAll() {
            return commonMapper.queryAll();
        }
    }

总结1:

1. 凡是异常被吞掉的,都不会执行回滚,数据会正常插入。 以后的案例中将不会再提到异常被自己捕获,但是不往上抛的情况,基本都是一样的逻辑、

2. 使用同一个Connection对象提交的数据,只要任何一处抛出异常 (一直往上抛,没有自己吞掉),那么会回滚这一次提交的所有数据。即使是不在同一张表,也会被回滚掉。 

3. 使用同一个Connection对象提交的数据,如果前面的方法抛出异常(一直往上抛,没有自己吞掉),那么后面的方法不会被执行。 以后的案例中将不会再提到此种情况,逻辑基本都是一样的。

案例5:

 areaService.addArea( area) 和 goodsService.addGoods(zgGoods)方法的事务传播属性都是 REQUIRED_NEW,但是在addGoods方法中抛异常。此时 areaService.addArea( area)正常插入数据。因为他们每次都新生成一个事务对象,并且持有新的Connection对象。回滚只是针对同一个Connection提交的SQL而言。因此,addArea正常插入数据。

AreaServiceImpl :

package com.xuexi.jack.service.area;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

//@PropertySource("classpath:config/core/core.properties")
@Service
public class AreaServiceImpl implements AreaService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private CommonMapper commonMapper;

    @Autowired
    AreaService areaService;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public int addArea(ConsultConfigArea area) {
        int i = commonMapper.addArea(area);
        //if(true) throw new RuntimeException("yic");
      /*  try {
            if (true) {throw new RuntimeException("111");}
        }
        catch (Exception e){}*/

        return 1;
    }
}
 GoodsServiceImpl :
    package com.xuexi.jack.service.goods;

    import com.xuexi.jack.dao.CommonMapper;
    import com.xuexi.jack.pojo.ZgGoods;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;

    import java.util.List;

    @Service
    public class GoodsServiceImpl implements GoodsService {

        @Autowired
        CommonMapper commonMapper;

        @Transactional(propagation = Propagation.REQUIRES_NEW)
        @Override
        public void addGoods(ZgGoods zgGoods) {
            int i = commonMapper.addGood(zgGoods);
            if(true) throw new RuntimeException("yic");
          /* try {
                if(true) throw new RuntimeException("yic");
            }catch (Exception e) { }
*/
        }

        @Transactional(readOnly = true)
        @Override
        public List<ZgGoods> queryAll() {
            return commonMapper.queryAll();
        }
    }

案例6: 特殊的传播属性 NESTED 错误使用

 areaService.addArea( area) 和 goodsService.addGoods(zgGoods)方法的事务传播属性都是 NESTED,但是在addGoods方法中抛异常。此时 areaService.addArea( area)也无法插入数据。NESTED是设置回滚点进行回滚的,如果我们抛异常,addGoods方法插入的数据就会被回滚掉。

但是,为什么前面的addArea插入的数据也会被回滚掉呢?因为addGoods 是被另一个事务方法 transation调用的,如果我们不处理异常,那么异常就会一直往上抛。因为NESTED 不会创建新的Connection对象,也就是说它和transation方法使用同一个Connection对象,异常一直往上抛,会被 transation 方法给捕获到并且给回滚掉。而 transation方法中也调用了addArea 方法,所以会被整体回滚掉,2张表都没有数据。

TransationServiceImpl :

package com.xuexi.jack.service.transaction;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import com.xuexi.jack.pojo.ZgGoods;
import com.xuexi.jack.pojo.ZgTicket;
import com.xuexi.jack.service.area.AreaService;
import com.xuexi.jack.service.goods.GoodsService;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service("transationServiceImpl")
public class TransationServiceImpl implements TransationService {

    @Autowired
    AreaService areaService;

    @Autowired
    GoodsService goodsService;

    @Autowired
    CommonMapper commonMapper;

    //开启了事务
    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void transation(ConsultConfigArea area, ZgGoods zgGoods) {
        //try {
            areaService.addArea( area);
            goodsService.addGoods(zgGoods);
       /* }catch (Exception e) {

        }*/
    }
    //提交事务


    @Transactional
    @Override
    public int getTicket() {

        //1、获取锁
        List<ZgTicket> zgTickets = commonMapper.queryTicketById("12306");
        Map lockmap = new HashMap();
        lockmap.put("ticketId", "12306");
        lockmap.put("version", zgTickets.get(0).getVersion());
        int i = commonMapper.updateLock(lockmap);

        if (i > 0) {
            //抢票
            ZgTicket zgTicket = zgTickets.get(0);
            zgTicket.setTicketCount(2);
            int i1 = commonMapper.updateTicket(zgTicket);
        } else {
            //继续抢
            ((TransationService) AopContext.currentProxy()).getTicket();
        }

        return 0;
    }

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Override
    public int getTicketModeOne() {

        Integer execute = transactionTemplate.execute(status -> {
            //1、获取锁
            List<ZgTicket> zgTickets = commonMapper.queryTicketById("12306");
            Map lockmap = new HashMap();
            lockmap.put("ticketId", "12306");
            lockmap.put("version", zgTickets.get(0).getVersion());
            int i = commonMapper.updateLock(lockmap);

            if (i > 0) {
                //抢票
                ZgTicket zgTicket = zgTickets.get(0);
                zgTicket.setTicketCount(2);
                int i1 = commonMapper.updateTicket(zgTicket);
            }
            return i;
        });

        if (execute == 0) {
            //继续抢
            getTicketModeOne();
        }
        return 0;
    }
}

AreaServiceImpl :

package com.xuexi.jack.service.area;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

//@PropertySource("classpath:config/core/core.properties")
@Service
public class AreaServiceImpl implements AreaService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private CommonMapper commonMapper;

    @Autowired
    AreaService areaService;

    @Transactional(propagation = Propagation.NESTED)
    @Override
    public int addArea(ConsultConfigArea area) {
        int i = commonMapper.addArea(area);
        return 1;
    }
}

GoodsServiceImpl :

    package com.xuexi.jack.service.goods;

    import com.xuexi.jack.dao.CommonMapper;
    import com.xuexi.jack.pojo.ZgGoods;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;

    import java.util.List;

    @Service
    public class GoodsServiceImpl implements GoodsService {

        @Autowired
        CommonMapper commonMapper;

        @Transactional(propagation = Propagation.NESTED)
        @Override
        public void addGoods(ZgGoods zgGoods) {
            int i = commonMapper.addGood(zgGoods);
            throw new RuntimeException("异常");
        }

        @Transactional(readOnly = true)
        @Override
        public List<ZgGoods> queryAll() {
            return commonMapper.queryAll();
        }
    }

案例6的解决方案:

想要按照回滚点进行回滚,那么异常是必须要抛出的,但是不能一直往上抛,否则会被整体回滚掉。因此,我们可以在addGoods方法中正常抛异常,想别的办法去依旧保留下之前addArea 方法插入的数据。

方法1:前面说过,回滚、提交都是针对同一个Connection对象的。那么,给addArea方法使用 REQUIRES_NEW 传播属性,这样他们2个就会使用不同的Connection对象了。

package com.xuexi.jack.service.area;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

//@PropertySource("classpath:config/core/core.properties")
@Service
public class AreaServiceImpl implements AreaService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private CommonMapper commonMapper;

    @Autowired
    AreaService areaService;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public int addArea(ConsultConfigArea area) {
        int i = commonMapper.addArea(area);
        return 1;
    }
}

方法2: 上一个解决方法并不好,因为如果有很多个方法,难到每一次出问题就要去修改传播属性值吗。其实,在addGoods方法中抛异常,但是在上一层方法 transation 中去捕获异常并吞掉异常,一样可以解决这个问题:

TransationServiceImpl:
package com.xuexi.jack.service.transaction;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import com.xuexi.jack.pojo.ZgGoods;
import com.xuexi.jack.pojo.ZgTicket;
import com.xuexi.jack.service.area.AreaService;
import com.xuexi.jack.service.goods.GoodsService;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service("transationServiceImpl")
public class TransationServiceImpl implements TransationService {

    @Autowired
    AreaService areaService;

    @Autowired
    GoodsService goodsService;

    @Autowired
    CommonMapper commonMapper;

    //开启了事务
    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void transation(ConsultConfigArea area, ZgGoods zgGoods) {
        try {
            areaService.addArea( area);
            goodsService.addGoods(zgGoods);
        }catch (Exception e) {}
    }
    //提交事务


    @Transactional
    @Override
    public int getTicket() {

        //1、获取锁
        List<ZgTicket> zgTickets = commonMapper.queryTicketById("12306");
        Map lockmap = new HashMap();
        lockmap.put("ticketId", "12306");
        lockmap.put("version", zgTickets.get(0).getVersion());
        int i = commonMapper.updateLock(lockmap);

        if (i > 0) {
            //抢票
            ZgTicket zgTicket = zgTickets.get(0);
            zgTicket.setTicketCount(2);
            int i1 = commonMapper.updateTicket(zgTicket);
        } else {
            //继续抢
            ((TransationService) AopContext.currentProxy()).getTicket();
        }

        return 0;
    }

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Override
    public int getTicketModeOne() {

        Integer execute = transactionTemplate.execute(status -> {
            //1、获取锁
            List<ZgTicket> zgTickets = commonMapper.queryTicketById("12306");
            Map lockmap = new HashMap();
            lockmap.put("ticketId", "12306");
            lockmap.put("version", zgTickets.get(0).getVersion());
            int i = commonMapper.updateLock(lockmap);

            if (i > 0) {
                //抢票
                ZgTicket zgTicket = zgTickets.get(0);
                zgTicket.setTicketCount(2);
                int i1 = commonMapper.updateTicket(zgTicket);
            }
            return i;
        });

        if (execute == 0) {
            //继续抢
            getTicketModeOne();
        }
        return 0;
    }
}

AreaServiceImpl
package com.xuexi.jack.service.area;

import com.xuexi.jack.dao.CommonMapper;
import com.xuexi.jack.pojo.ConsultConfigArea;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

//@PropertySource("classpath:config/core/core.properties")
@Service
public class AreaServiceImpl implements AreaService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private CommonMapper commonMapper;

    @Autowired
    AreaService areaService;

    @Transactional(propagation = Propagation.NESTED)
    @Override
    public int addArea(ConsultConfigArea area) {
        int i = commonMapper.addArea(area);
        return 1;
    }
}

GoodsServiceImpl:
    package com.xuexi.jack.service.goods;

    import com.xuexi.jack.dao.CommonMapper;
    import com.xuexi.jack.pojo.ZgGoods;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;

    import java.util.List;

    @Service
    public class GoodsServiceImpl implements GoodsService {

        @Autowired
        CommonMapper commonMapper;

        @Transactional(propagation = Propagation.NESTED)
        @Override
        public void addGoods(ZgGoods zgGoods) {
            int i = commonMapper.addGood(zgGoods);
            throw new RuntimeException("异常");
        }

        @Transactional(readOnly = true)
        @Override
        public List<ZgGoods> queryAll() {
            return commonMapper.queryAll();
        }
    }

3. 带着问题看源码

3.1 类分析:

上一篇介绍了@Bean方法的实例化,接下来我们看一下事务支持类ProxyTransactionManagementConfiguration都干了什么事情,

/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.transaction.annotation;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.transaction.config.TransactionManagementConfigUtils;
import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionInterceptor;

/**
 * {@code @Configuration} class that registers the Spring infrastructure beans
 * necessary to enable proxy-based annotation-driven transaction management.
 *
 * @author Chris Beams
 * @since 3.1
 * @see EnableTransactionManagement
 * @see TransactionManagementConfigurationSelector
 */
@Configuration
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {

	/*
	* 明显是创建事务切面实例
	* BeanFactoryTransactionAttributeSourceAdvisor
	*
	* */
	@Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {
		BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
		advisor.setTransactionAttributeSource(transactionAttributeSource());
		//设置通知类
		advisor.setAdvice(transactionInterceptor());
		if (this.enableTx != null) {
			advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
		}
		return advisor;
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public TransactionAttributeSource transactionAttributeSource() {
		return new AnnotationTransactionAttributeSource();
	}

	/*
	* 创建事务advice
	* TransactionInterceptor
	* */
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public TransactionInterceptor transactionInterceptor() {
		TransactionInterceptor interceptor = new TransactionInterceptor();
		interceptor.setTransactionAttributeSource(transactionAttributeSource());
		//事务管理器要跟数据源挂钩,所以需要自己定义
		if (this.txManager != null) {
			interceptor.setTransactionManager(this.txManager);
		}
		return interceptor;
	}

}

1. 在我们调用 transactionAdvisor方法的时候,我们生成了advisor对象

2. 在advisor对象中,我们通过advisor.setTransactionAttributeSource(transactionAttributeSource()) 的调用,生成了事务注解属性类,负责生成包装事务属性的包装类并进行注入

3. 在advisor对象中,我们通过advisor.setAdvice(transactionInterceptor()), 还生成了advice类,也就是最小单元的advisor, 负责具体的拦截、增强的类

4. 最后给advisor设置优先级,确保优先执行

3.2 源码分析

Spring的事务是AOP技术实现的,因此也会走AOP的那一套流程进行调用,最终调用到了事务切面。

 

贴出这个方法的全部源码,对字方法进行逐步分析:

@Nullable
	protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

		// If the transaction attribute is null, the method is non-transactional.
		//获取事务属性类 AnnotationTransactionAttributeSource
		TransactionAttributeSource tas = getTransactionAttributeSource();

		//获取方法上面有@Transactional注解的属性
		final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);

		//获取事务管理器
		final PlatformTransactionManager tm = determineTransactionManager(txAttr);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
			// Standard transaction demarcation with getTransaction and commit/rollback calls.
			TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
			Object retVal = null;
			try {
				// This is an around advice: Invoke the next interceptor in the chain.
				// This will normally result in a target object being invoked.
				//火炬传递
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// target invocation exception
				//事务回滚
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				cleanupTransactionInfo(txInfo);
			}
			//事务提交
			commitTransactionAfterReturning(txInfo);
			return retVal;
		}

		else {
			final ThrowableHolder throwableHolder = new ThrowableHolder();

			// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
			try {
				Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, status -> {
					TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
					try {
						return invocation.proceedWithInvocation();
					}
					catch (Throwable ex) {
						if (txAttr.rollbackOn(ex)) {
							// A RuntimeException: will lead to a rollback.
							if (ex instanceof RuntimeException) {
								throw (RuntimeException) ex;
							}
							else {
								throw new ThrowableHolderException(ex);
							}
						}
						else {
							// A normal return value: will lead to a commit.
							throwableHolder.throwable = ex;
							return null;
						}
					}
					finally {
						cleanupTransactionInfo(txInfo);
					}
				});

				// Check result state: It might indicate a Throwable to rethrow.
				if (throwableHolder.throwable != null) {
					throw throwableHolder.throwable;
				}
				return result;
			}
			catch (ThrowableHolderException ex) {
				throw ex.getCause();
			}
			catch (TransactionSystemException ex2) {
				if (throwableHolder.throwable != null) {
					logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
					ex2.initApplicationException(throwableHolder.throwable);
				}
				throw ex2;
			}
			catch (Throwable ex2) {
				if (throwableHolder.throwable != null) {
					logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
				}
				throw ex2;
			}
		}
	}

 这里面的方法都很重要,比如 TransactionAttributeSource实例的获取,PlatformTransactionManager的获取都值得着重去了解,但是今天的核心是事务传播属性的使用与底层分析。因此,事务的创建、拦截、回滚是本文的核心。

3.2.1 事务创建

 进入这个方法

来到核心方法处:

下面针对这个方法,逐步进行代码分析:

1.  Object transaction = doGetTransaction() 获取数据源对象。其实,每次进来都是 new了一个包装数据源的对象。第一次进来,是没有DataSource数据源对象的,因此, ConnectionHolder为null, 我们拿不到连接数据库的信息。

	@Override
	protected Object doGetTransaction() {
		//管理connection对象,创建回滚点,按照回滚点回滚,释放回滚点
		DataSourceTransactionObject txObject = new DataSourceTransactionObject();

		//DataSourceTransactionManager默认是允许嵌套事务的
		txObject.setSavepointAllowed(isNestedTransactionAllowed());

		//obtainDataSource() 获取数据源对象,其实就是数据库连接块对象
		ConnectionHolder conHolder =
				(ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
		txObject.setConnectionHolder(conHolder, false);
		return txObject;
	}

2、 第一次进入,其实核心就是设置连接数据库的信息 

看看具体设置了写什么东西:注意一下 newTransaction这个参数,这是数据回滚的依据,这里默认是true

 概括起来就是,本文一开头常用的3个传播属性,第一次进入的时候都会执行相同的操作:

a. 挂起之前的事务,核心就是将之前存储的Connection对象设置为空,并进行数据库还原操作,具体看        SuspendedResourcesHolder suspendedResources = suspend(null) 代码:

b. doBegin(transaction, definition) 开启事务。其实就是获取数据库连接信息,就是我们配置的信息

 

c. 关闭数据库的自动提交功能,

d. 搜集DataSource 和 Connection的映射放入map中。

2.  返回到 createTransactionIfNecessary 方法中继续往下走, 此时我们我们依旧获取到了连接数据库的信息,当前事务的状态(是不是新事务)等信息。

3. 最后,就是线程绑定,放入到ThreadLocal中:

 

 3.2.2 事务的链式调用

 到这一步,就是事务的链式调用了。

源码case:

 @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void transation(ConsultConfigArea area, ZgGoods zgGoods) {
     
            areaService.addArea( area);
            goodsService.addGoods(zgGoods);
       
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public int addArea(ConsultConfigArea area) {
        int i = commonMapper.addArea(area);
        return 1;
    }
   @Transactional(propagation = Propagation.NESTED)
        @Override
        public void addGoods(ZgGoods zgGoods) {
            int i = commonMapper.addGood(zgGoods);
            throw new RuntimeException("异常");
        }

1、 我们在调用 transation 方法的时候, 由于它使用了 @Transactional(propagation = Propagation.REQUIRED)注解并且它是第一次调用,我们会在调用实际的transation方法之前先进入代理类,进行事务相关处理。 由于是第一次次进入,我们会生成一个事务,并且事务的 newTransation为true。

2. 调用 retVal = invocation.proceedWithInvocation() 进行火炬传递,也就是链式调用。

3. 我们会进入AreaServiceImpl的代理类,也就是再次把事务的相关流程再走一遍。由于在addArea方法的时候,我们使用了注解 @Transactional(propagation = Propagation.REQUIRES_NEW)。也就是说,挂起之前的事务并生成一个全新的事务,newTransation为true。

4. 调用 retVal = invocation.proceedWithInvocation() 进行火炬传递,继续链式调用。

5. 我们会进入GoodsServiceImpl 的代理类,也就是再次把事务的相关流程再走一遍。由于在addGoods方法的时候,我们使用了注解 @Transactional(propagation = Propagation.NESTED)。他会生成一个回滚点。针对 NESTED 这种传播属性传播属性并且是同一个线程再次创建事务对象,我们会把 newTransation设置为FALSE

6. 最后按顺序依次调用 

TransationServiceImpl.transation() --> areaService.addArea( area) --> goodsService.addGoods(zgGoods);

针对同一个线程,反复通过链式调用创建事务的过程。刚刚说了第一次创建事务,newTransation为true。 针对第2 、3、4...... 次的创建事务。我们会执行执行以下流程:

我们会根据不同的传播属性,执行不同的业务逻辑。核心就是处理连接数据库的connection对象实例。

如果是 传播属性是 PROPAGATION_REQUIRES_NEW,我们会完全在创建一个新的、独立的事务。newTransation为true

如果是 PROPAGATION_NESTED,我们继续使用之前的事务,也就是在调用transation 方法的时候创建的事务。newTransation为 false

3.2.3 事务的回滚。

由于我们执行最后一个方法的时候抛了异常信息:

 @Transactional(propagation = Propagation.NESTED)
        @Override
        public void addGoods(ZgGoods zgGoods) {
            int i = commonMapper.addGood(zgGoods);
            throw new RuntimeException("异常");
        }

我们会进行异常的处理逻辑,处理完以后接着往上抛异常。

如何处理回滚的呢?

 

 最终的回滚方法如下:

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
		try {
			boolean unexpectedRollback = unexpected;

			try {
				triggerBeforeCompletion(status);

				//按照嵌套事务按照回滚点回滚
				if (status.hasSavepoint()) {
					if (status.isDebug()) {
						logger.debug("Rolling back transaction to savepoint");
					}
					status.rollbackToHeldSavepoint();
				}
				//都为PROPAGATION_REQUIRED最外层事务统一回滚
				else if (status.isNewTransaction()) {
					if (status.isDebug()) {
						logger.debug("Initiating transaction rollback");
					}
					doRollback(status);
				}
				else {
					// Participating in larger transaction
					if (status.hasTransaction()) {
						if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
							if (status.isDebug()) {
								logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
							}
							doSetRollbackOnly(status);
						}
						else {
							if (status.isDebug()) {
								logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
							}
						}
					}
					else {
						logger.debug("Should roll back transaction but cannot - no transaction available");
					}
					// Unexpected rollback only matters here if we're asked to fail early
					if (!isFailEarlyOnGlobalRollbackOnly()) {
						unexpectedRollback = false;
					}
				}
			}
			catch (RuntimeException | Error ex) {
				triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
				throw ex;
			}

			triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

			// Raise UnexpectedRollbackException if we had a global rollback-only marker
			if (unexpectedRollback) {
				throw new UnexpectedRollbackException(
						"Transaction rolled back because it has been marked as rollback-only");
			}
		}
		finally {
			cleanupAfterCompletion(status);
		}
	}

1、首先就是判断右没有回滚点,如果有回滚点就按照回滚点进行回滚;

2.  如果没有回滚点,判断newTransation 是否为true,为true则直接回滚。

3.  既没有回滚点,newTransation 也为FALSE,走默认逻辑,最后接着往上抛异常;找到上一层调用方法,继续仅需1、2步逻辑判断,直到回滚为止。

4. 因为回滚是基于Connection进行的,如果部分子方法已经插入数据,并且传播属性为PROPAGATION_NEW, 那么这个子方法插入的数据不会回滚。

总结2:

  @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void transation(ConsultConfigArea area, ZgGoods zgGoods) {

            areaService.addArea( area);
            goodsService.addGoods(zgGoods);

    }

本文的测试case中, transation方法使用的是REQUIRED,  addArea方法使用的是REQUIRED_NEW,  addGoods方法使用的是NESTED。 

addGoods抛异常,代码会按照回滚点进行回滚,然后接着往上抛异常;transation方法捕获到异常并且newTransation 为true,所以会直接回滚所有使用相同Connection对象进行数据库操作的子方法,即全员回滚。

但是,addArea方法的传播属性为REQUIRED_NEW, 它用于独立的Connection对象,与transation方法中持有Connection对象不是同一个,因此在transation捕获到异常并且调用它的connection进行回滚的时候,addArea方法插入的数据不会被回滚掉。

 

3.2.4. 伪代码分析:

try {
				// This is an around advice: Invoke the next interceptor in the chain.
				// This will normally result in a target object being invoked.
				//火炬传递
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// target invocation exception
				//事务回滚
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}

其实核心伪代码就是以上这些。

假设 存在3个处理异常的一个类,并且这个类具有3个不同的实例a 、 b、 c, 那么久可以改写成这样:

        try {
		        a.transation()
			}
			catch (Throwable ex) {
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
        try {
		        b.addArea();
			}
			catch (Throwable ex) {
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
        try {
		        c.addGoods();
			}
			catch (Throwable ex) {
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}

a.transation()内部调用了b.addArea()  和 c.addGoods(); 

c.addGoods() 内部抛异常进行回滚,继续往上抛异常;

a.transation()捕获到异常,也进行回滚,此时回滚掉所有字方法更新的数据,接着往上抛异常;

由于回滚用到的是相同的connection对象,b.addArea()生成的事务中的connection不同于a.transation()生成的connection,因此b.addArea()更新的数据不会被回滚。

伪代码的方式进行分析,是非常款速、简洁的,可以重点使用;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值