Spring系列第46篇:Spring如何管理多数据源事务?

}

6、Ds2User2Service

用来操作ds2.user2表,对应事务管理器transactionManager2

package com.javacode2018.tx.demo7;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.stereotype.Component;

import org.springframework.transaction.annotation.Propagation;

import org.springframework.transaction.annotation.Transactional;

@Component

public class Ds2User2Service {

@Autowired

private JdbcTemplate jdbcTemplate2;

@Transactional(transactionManager = “transactionManager2”, propagation = Propagation.REQUIRED)

public void required(String name) {

this.jdbcTemplate2.update(“insert into user2(name) VALUES (?)”, name);

}

}

7、Tx1Service

package com.javacode2018.tx.demo7;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

@Component

public class Tx1Service {

@Autowired

private Ds1User1Service ds1User1Service;

@Autowired

private Ds1User2Service ds1User2Service;

}

8、Tx2Service

package com.javacode2018.tx.demo7;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

@Component

public class Tx2Service {

@Autowired

private Ds2User1Service ds2User1Service;

@Autowired

private Ds2User2Service ds2User2Service;

}

9、测试类Demo7Test

package com.javacode2018.tx.demo7;

import org.junit.After;

import org.junit.Before;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import org.springframework.jdbc.core.JdbcTemplate;

public class Demo7Test {

private Tx1Service txService1;

private JdbcTemplate jdbcTemplate1;

private JdbcTemplate jdbcTemplate2;

//@Before标注的方法会在任意@Test方法执行之前执行,我们这在里清理一下2库中4张表的数据

@Before

public void before() {

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig7.class);

txService1 = context.getBean(Tx1Service.class);

this.jdbcTemplate1 = context.getBean(“jdbcTemplate1”, JdbcTemplate.class);

this.jdbcTemplate2 = context.getBean(“jdbcTemplate2”, JdbcTemplate.class);

jdbcTemplate1.update(“truncate table ds1.user1”);

jdbcTemplate1.update(“truncate table ds1.user2”);

jdbcTemplate2.update(“truncate table ds2.user1”);

jdbcTemplate2.update(“truncate table ds2.user2”);

}

//@After标注的方法会在任意@Test方法执行完毕之后执行,我们在此处输出4张表的数据,用来查看测试案例之后,表中的数据清空

@After

public void after() {

System.out.println(“ds1.user1表数据:” + this.jdbcTemplate1.queryForList(“SELECT * from user1”));

System.out.println(“ds1.user2表数据:” + this.jdbcTemplate1.queryForList(“SELECT * from user2”));

System.out.println(“ds2.user1表数据:” + this.jdbcTemplate2.queryForList(“SELECT * from user1”));

System.out.println(“ds2.user2表数据:” + this.jdbcTemplate2.queryForList(“SELECT * from user2”));

}

}

代码验证

1、场景1

外围方法和内部方法使用相同的事务管理器,传播行为都是REQUIRED。

Tx1Service中添加代码

@Transactional(transactionManager = “transactionManager1”, propagation = Propagation.REQUIRED)

public void test1() {

this.ds1User1Service.required(“张三”);

this.ds1User2Service.required(“李四”);

throw new RuntimeException();

}

方法、事务管理器、事务管理器对应数据源、操作db的jdbctemplate中数据源对应关系。

| 方法 | 事务管理器 | 事务管理器对应数据源 | jdbctemplate对应数据源 |

| — | — | — | — |

| test1 | transactionManager1 | datasource1 | - |

| ds1User1Service.required | transactionManager1 | datasource1 | datasource1 |

| this.ds1User2Service.required | transactionManager1 | datasource1 | datasource1 |

Demo7Test中添加测试用例

@Test

public void test1() {

this.txService1.test1();

}

运行输出

ds1.user1表数据:[]

ds1.user2表数据:[]

ds2.user1表数据:[]

ds2.user2表数据:[]

结论分析

| 数据库结果 | 结果分析 |

| — | — |

| “张三”、“李四”均为插入 | 外围方法和内部方法使用同一个事务管理器transactionManager1,且事务管理器和jdbctemplate的datasource都是同一个,外围方法会开启事务,内部方法加入外围方法事务,外围方法弹出异常导致事务回滚,内部方法跟着回滚了。 |

2、场景2

外部方法和内部方法使用不同的事务管理器。

Tx1Service中添加代码

@Transactional(transactionManager = “transactionManager2”, propagation = Propagation.REQUIRED)

public void test2() {

this.ds1User1Service.required(“张三”);

this.ds1User2Service.required(“李四”);

throw new RuntimeException();

}

方法、事务管理器、事务管理器对应数据源、操作db的jdbctemplate中数据源对应关系。

| 方法 | 事务管理器 | 事务管理器对应数据源 | jdbctemplate对应数据源 |

| — | — | — | — |

| test2 | transactionManager2 | datasource2 | - |

| ds1User1Service.required(“张三”); | transactionManager1 | datasource1 | datasource1 |

| this.ds1User2Service.required(“李四”); | transactionManager1 | datasource1 | datasource1 |

Demo7Test中添加测试用例

@Test

public void test2() {

this.txService1.test2();

}

运行输出

ds1.user1表数据:[{id=1, name=张三}]

ds1.user2表数据:[{id=1, name=李四}]

ds2.user1表数据:[]

ds2.user2表数据:[]

结论分析

| 数据库结果 | 结果分析 |

| — | — |

| “张三”、"李四"均插入 | 外围方法test2和内部两个required方法用到的不是同一个事务管理器,内部的2个方法在各自的事务中执行,不受外部方法事务的控制。 |

3、场景3
Tx1Service中添加代码

@Autowired

private Ds2User1Service ds2User1Service;

@Autowired

private Ds2User2Service ds2User2Service;

@Transactional(transactionManager = “transactionManager1”, propagation = Propagation.REQUIRED)

public void test3() {

this.ds1User1Service.required(“张三”);

this.ds1User2Service.required(“李四”);

this.ds2User1Service.required(“王五”);

this.ds2User2Service.required(“赵六”);

throw new RuntimeException();

}

方法、事务管理器、事务管理器对应数据源、操作db的jdbctemplate中数据源对应关系。

| 方法 | 事务管理器 | 事务管理器对应数据源 | jdbctemplate对应数据源 |

| — | — | — | — |

| test3 | transactionManager1 | datasource1 | - |

| this.ds1User1Service.required(“张三”); | transactionManager1 | datasource1 | datasource1 |

| this.ds1User2Service.required(“李四”); | transactionManager1 | datasource1 | datasource1 |

| this.ds2User1Service.required(“王五”); | transactionManager2 | datasource2 | datasource2 |

| this.ds2User2Service.required(“赵六”); | transactionManager2 | datasource2 | datasource2 |

Demo7Test中添加测试用例

@Test

public void test3() {

this.txService1.test3();

}

运行输出

ds1.user1表数据:[]

ds1.user2表数据:[]

ds2.user1表数据:[{id=1, name=王五}]

ds2.user2表数据:[{id=1, name=赵六}]

结论分析

“张三”、"李四"都未插入,“王五”、“赵六”插入成功。

外围方法和内部的前2个required方法事务管理器都是transactionManager1,所以他们3个在一个事务中执行;而内部的后2个required方法事务管理器是transactionManager2,他们分别在自己的事务中执行,不受外围方法事务的控制,外围方法感受到了异常,回滚事务,只会导致内部的前2个required方法回滚。

4、场景4
Tx2Service中加入代码

@Transactional(transactionManager = “transactionManager2”, propagation = Propagation.REQUIRED)

public void test1() {

this.ds2User1Service.required(“王五”);

this.ds2User2Service.required(“赵六”);

}

Tx1Service中加入代码

@Autowired

private Tx2Service tx2Service;

@Transactional(transactionManager = “transactionManager1”, propagation = Propagation.REQUIRED)

public void test4() {

this.ds1User1Service.required(“张三”);

this.ds1User2Service.required(“李四”);

this.tx2Service.test1();

throw new RuntimeException();

}

方法、事务管理器、事务管理器对应数据源、操作db的jdbctemplate中数据源对应关系。

| 方法 | 事务管理器 | 事务管理器对应数据源 | jdbctemplate对应数据源 |

| — | — | — | — |

| test4 | transactionManager1 | datasource1 | - |

| this.ds1User1Service.required | transactionManager1 | datasource1 | datasource1 |

| this.ds1User2Service.required | transactionManager1 | datasource1 | datasource1 |

| this.tx2Service.test1() | transactionManager2 | datasource2 | - |

| this.ds2User1Service.required | transactionManager2 | datasource2 | datasource2 |

| this.ds2User2Service.required | transactionManager2 | datasource2 | datasource2 |

Demo7Test中添加测试用例

@Test

public void test4() {

this.txService1.test4();

}

运行输出

ds1.user1表数据:[]

ds1.user2表数据:[]

ds2.user1表数据:[{id=1, name=王五}]

ds2.user2表数据:[{id=1, name=赵六}]

结论分析

“张三”、"李四"都未插入,“王五”、“赵六”插入成功。

分析一下过程

1、test4在事务管理器transactionManager1中开启事务tm1,并将连接放入resourceThreadLocal中(datasource1->conn1)

2、this.ds1User1Service.required(“张三”)事务管理器是transactionManager1,所以会加入事务tm1中,通过jdbctemplate1插入张三,由于jdbctemplate1.datasource是datasource1,所以会获取到threadlocal中的conn1来插入数据

3、this.ds1User2Service.required(“李四”)事务管理器是transactionManager1,所以会加入事务tm1中,通过jdbctemplate1插入张三,由于jdbctemplate1.datasource是datasource1,所以会获取到threadlocal中的conn1来插入数据

4、执行this.tx2Service.test1(),这个方法事务管理器是transactionManager2,所以会重新开启一个事务tm2,并将连接放入resourceThreadLocal中(datasource2->conn2)

5、this.ds2User1Service.required(“王五”)事务管理器是transactionManager2,通过所以会加入事务tm2中,通过jdbctemplate1插入张三,由于jdbctemplate1.datasource是datasource2,所以会获取到threadlocal中的conn2来插入数据

6、this.ds2User2Service.required(“赵六”)事务管理器是transactionManager2,所以会加入事务tm2中,通过jdbctemplate1插入张三,由于jdbctemplate1.datasource是datasource2,所以会获取到threadlocal中的conn2来插入数据

7、tm2提交

8、tm1发现test4抛出异常,tm1执行回滚

5、场景5
Tx2Service中加入代码

@Transactional(transactionManager = “transactionManager2”, propagation = Propagation.REQUIRED)

public void test2() {

this.ds2User1Service.required(“王五”);

this.ds2User2Service.required(“赵六”);

throw new RuntimeException();

}

Tx1Service中加入代码

@Transactional(transactionManager = “transactionManager1”, propagation = Propagation.REQUIRED)

public void test5() {

this.ds1User1Service.required(“张三”);

this.ds1User2Service.required(“李四”);

this.tx2Service.test2();

}

方法、事务管理器、事务管理器对应数据源、操作db的jdbctemplate中数据源对应关系。

| 方法 | 事务管理器 | 事务管理器对应数据源 | jdbctemplate对应数据源 |

| — | — | — | — |

| test4 | transactionManager1 | datasource1 | - |

| this.ds1User1Service.required | transactionManager1 | datasource1 | datasource1 |

| this.ds1User2Service.required | transactionManager1 | datasource1 | datasource1 |

| this.tx2Service.test1() | transactionManager2 | datasource2 | - |

| this.ds2User1Service.required | transactionManager2 | datasource2 | datasource2 |

| this.ds2User2Service.required | transactionManager2 | datasource2 | datasource2 |

Demo7Test中添加测试用例

@Test

public void test5() {

this.txService1.test5();

}

运行输出

ds1.user1表数据:[]

ds1.user2表数据:[]

ds2.user1表数据:[]

ds2.user2表数据:[]

结论分析

4个表都未插入数据。

外围方法test5通过事务管理器transactionManager1开启了事务tm1,内部方法插入“张三”,“李四”加入了tm1事务,而test2通过事务管理器transactionManager2又开启了一个事务tm2,test2内部方法插入“王五”,“赵六”加入了tm2事务,test2内部抛出了异常,tm2和tm1都感受到了这个异常,所以2个事务都进行了回滚操作。

案例2


spring配置类

package com.javacode2018.tx.demo8;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import org.springframework.transaction.PlatformTransactionManager;

import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@EnableTransactionManagement //开启spring事务管理功能

@Configuration //指定当前类是一个spring配置类

@ComponentScan //开启bean扫描注册

public class MainConfig8 {

//定义数据源1,连接数据库:ds1

@Bean

public DataSource dataSource1() {

org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();

dataSource.setDriverClassName(“com.mysql.jdbc.Driver”);

dataSource.setUrl(“jdbc:mysql://localhost:3306/ds1?characterEncoding=UTF-8”);

dataSource.setUsername(“root”);

dataSource.setPassword(“root123”);

dataSource.setInitialSize(5);

return dataSource;

}

//定义一个jdbcTemplate1

@Bean

public JdbcTemplate jdbcTemplate1(DataSource dataSource) {

return new JdbcTemplate(dataSource);

}

//定义事务管理器transactionManager1

@Bean

public PlatformTransactionManager transactionManager1(DataSource dataSource) {

return new DataSourceTransactionManager(dataSource);

}

//定义jdbcTemplate2

@Bean

public JdbcTemplate jdbcTemplate2(DataSource dataSource) {

return new JdbcTemplate(dataSource);

}

//定义事务管理器transactionManager2

@Bean

public PlatformTransactionManager transactionManager2(DataSource dataSource) {

return new DataSourceTransactionManager(dataSource);

}

}

上面代码中

  • 定义了1个数据源:dataSource1

  • 2个jdbctemplate:jdbcTemplate1和jdbcTemplate2,他们的datasource都是dataSource1

  • 2个事务管理器:transactionManager1和transactionManager2,他们的datasource都是dataSource1

有同学发现这样写是不是很奇怪,不是说一个数据源定义一个事务管理器么,这什么操作?

不急,我们这样写,是为了让你更深入了解其原理。

User2Service

内部的required方法操作db用的是jdbcTemplate2,事务管理器为transactionManager2

package com.javacode2018.tx.demo8;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.stereotype.Component;

import org.springframework.transaction.annotation.Propagation;

import org.springframework.transaction.annotation.Transactional;

@Component

public class User2Service {

@Autowired

private JdbcTemplate jdbcTemplate2;

@Transactional(transactionManager = “transactionManager2”, propagation = Propagation.REQUIRED)

public void required() {

this.jdbcTemplate2.update(“insert into user2(name) VALUES (?)”, “李四”);

}

}

User2Service

内部的required方法操作db用的是jdbcTemplate1,事务管理器为transactionManager1,并且会调用user2Service的required方法。

package com.javacode2018.tx.demo8;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.stereotype.Component;

import org.springframework.transaction.annotation.Propagation;

import org.springframework.transaction.annotation.Transactional;

@Component

public class User1Service {

@Autowired

private JdbcTemplate jdbcTemplate1;

@Autowired

private User2Service user2Service;

@Transactional(transactionManager = “transactionManager1”, propagation = Propagation.REQUIRED)

public void required() {

this.jdbcTemplate1.update(“insert into user1(name) VALUES (?)”, “张三”);

this.user2Service.required();

throw new RuntimeException();

}

}

大家觉得required方法执行完毕之后,会是什么结果?

A:张三未插入、李四插入成功

B:张三、李四均为插入

大家先思考一下,先别看下面的执行结果,可以参考事务管理器的执行过程分析一下结果。

好了,我们上测试用例。

Demo8Test

package com.javacode2018.tx.demo8;

import org.junit.After;

import org.junit.Before;

import org.junit.Test;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import org.springframework.jdbc.core.JdbcTemplate;

public class Demo8Test {

private User1Service user1Service;

private JdbcTemplate jdbcTemplate1;

//@Before标注的方法会在任意@Test方法执行之前执行,我们这在里清理一下2库中4张表的数据

@Before

public void before() {

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig8.class);

this.user1Service = context.getBean(User1Service.class);

this.jdbcTemplate1 = context.getBean(“jdbcTemplate1”, JdbcTemplate.class);

jdbcTemplate1.update(“truncate table ds1.user1”);

jdbcTemplate1.update(“truncate table ds1.user2”);

}

//@After标注的方法会在任意@Test方法执行完毕之后执行,我们在此处输出4张表的数据,用来查看测试案例之后,表中的数据清空

@After

public void after() {

System.out.println(“ds1.user1表数据:” + this.jdbcTemplate1.queryForList(“SELECT * from user1”));

System.out.println(“ds1.user2表数据:” + this.jdbcTemplate1.queryForList(“SELECT * from user2”));

}

@Test

public void test1() {

this.user1Service.required();

}

}

运行输出

ds1.user1表数据:[]

ds1.user2表数据:[]

结果是都没有插入。

结果分析

分析一下执行过程

1、this.user1Service.required();

2、事务拦截器拦截user1Service.required()方法,事务配置信息:(事务管理器:transactionManager1,传播行为REQUIRED)

3、问一下transactionManager1,当前是否有事务,transactionManager2看了一下,发现没有,那么重新创建一个事务tm1,通过transactionManager1中的datasource,即datasource1重新获取一个连接:conn1,然后丢到resourceThreadLocal中(datasource1->conn1)

4、执行this.jdbcTemplate1.update(“insert into user1(name) VALUES (?)”, “张三”),由于jdbcTemplate1中的datasource是datasource1,所以会从resourceThreadLocal中拿到conn1连接来执行sql

5、执行this.user2Service.required();

6、事务拦截器拦截user1Service.required()方法,事务配置信息:(事务管理器:transactionManager2,传播行为REQUIRED)

7、问一下transactionManager2,当前是否有事务?大家在回头看一下事务管理器是如何判断当前是否有事务的,由于transactionManager2和transactionManager1用到的都是datasource1,所以transactionManager2会发现当前是存在事务的,即tm1

8、执行this.jdbcTemplate2.update(“insert into user2(name) VALUES (?)”, “李四”),由于jdbcTemplate2中的datasource也是datasource1,所以会从resourceThreadLocal中拿到conn1连接来执行sql

9、最终整个操作过程中只有一个事务tm1,一个连接conn1,通过conn1执行2个插入操作

10、执行throw new RuntimeException();抛出异常

11、tm1感受到了异常,所以会执行回滚操作,最终都插入失败

总结一下


1、本文介绍了多数据源事务的使用,2个步骤:先为每个数据源定义一个事务管理器,然后在@Transactional中指定具体要使用哪个事务管理器。

2、事务管理器运行过程、事务管理器如何判断当前是否有事务,这2点非常非常重要,大家再看一下

有问题欢迎留言交流。

案例源码


自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

难道这样就够了吗?不,远远不够!

提前多熟悉阿里往年的面试题肯定是对面试有很大的帮助的,但是作为技术性职业,手里有实打实的技术才是你面对面试官最有用的利器,这是从内在散发出来的自信。

备战阿里时我花的最多的时间就是在学习技术上,占了我所有学习计划中的百分之70,这是一些我学习期间觉得还是很不错的一些学习笔记

我为什么要写这篇文章呢,其实我觉得学习是不能停下脚步的,在网络上和大家一起分享,一起讨论,不单单可以遇到更多一样的人,还可以扩大自己的眼界,学习到更多的技术,我还会在csdn、博客、掘金等网站上分享技术,这也是一种学习的方法。

今天就分享到这里了,谢谢大家的关注,以后会分享更多的干货给大家!

阿里一面就落马,恶补完这份“阿里面试宝典”后,上岸蚂蚁金服

阿里一面就落马,恶补完这份“阿里面试宝典”后,上岸蚂蚁金服

image.png

非常非常重要,大家再看一下**。

有问题欢迎留言交流。

案例源码


自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-qzcpkQLE-1712113926155)]
[外链图片转存中…(img-tdYBGXKG-1712113926155)]
[外链图片转存中…(img-5csFScBz-1712113926156)]
[外链图片转存中…(img-KfHunwuA-1712113926156)]
[外链图片转存中…(img-UsvW6odb-1712113926157)]
[外链图片转存中…(img-kRyzw2H0-1712113926157)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-t2JCuddB-1712113926157)]

难道这样就够了吗?不,远远不够!

提前多熟悉阿里往年的面试题肯定是对面试有很大的帮助的,但是作为技术性职业,手里有实打实的技术才是你面对面试官最有用的利器,这是从内在散发出来的自信。

备战阿里时我花的最多的时间就是在学习技术上,占了我所有学习计划中的百分之70,这是一些我学习期间觉得还是很不错的一些学习笔记

我为什么要写这篇文章呢,其实我觉得学习是不能停下脚步的,在网络上和大家一起分享,一起讨论,不单单可以遇到更多一样的人,还可以扩大自己的眼界,学习到更多的技术,我还会在csdn、博客、掘金等网站上分享技术,这也是一种学习的方法。

今天就分享到这里了,谢谢大家的关注,以后会分享更多的干货给大家!

[外链图片转存中…(img-OcUVfzGM-1712113926158)]

[外链图片转存中…(img-bKpy6fyR-1712113926158)]

[外链图片转存中…(img-kbdaqh8a-1712113926158)]

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值