四、springCloudAlibaba基础篇(分布式事务)
前言
一、环境准备
二、依赖列表
三、代码编写
3.1、共用配置
3.2、服务提供者代码编写
3.3、服务调用者代码编写
3.4、测试方式
前言
下面是一个用于插入用户的代码片段,在插入用户前会记录下用户操作,图中因为 1 / 0 必然会导致异常,导致后半部分代码不会执行,此时为了保证事务的原子性,插入用户操作记录也应该回滚,spring为我们提供的声明式事务只用添加@Transactional就可以达到效果。
现在我们的项目是微服务架构,我们调用的方法可能是另一台机器的,那么事务还会生效吗?答案是不会的,此时可以使用分布式事务seata解决,
Seata 是由阿里巴巴开发的一款开源的分布式事务解决方案,官网都是中文的: seata官网
@Autowired
private UsersRespository userRespository;
@Autowired
private UsersLogRespository usersLogRespository;
//@Autowired
//private UsersLogService usersLogService;
@Override
@Transactional
public Users insertUser2() {
UsersLog usersLog = new UsersLog();
usersLog.setMsg("用户创建");
usersLog = usersLogRespository.save(usersLog);
//usersLog = usersLogService.save(usersLog);
int i = 1 / 0;
Users u = new Users();
u.setName(UUID.randomUUID().toString());
u.setLogId(usersLog.getLogId());
userRespository.save(u);
return null;
}
一、环境准备
步骤一:铺好被褥准备一会下载时睡一觉
步骤二:进入到seata官网点击下载,俗话说的好,不用最新版,跳过第二版,所以我选择了1.4.2版本,找个目录解压
步骤三:修改conf目录中的file.conf配置文件,设置事务日志存储模式用数据库,并自定义一个事务组。并在连接的数据库中执行以下SQL语句
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
);
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT ,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256) ,
`lock_key` VARCHAR(128) ,
`branch_type` VARCHAR(8) ,
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
);
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` LONG ,
`branch_id` LONG,
`resource_id` VARCHAR(256) ,
`table_name` VARCHAR(32) ,
`pk` VARCHAR(36) ,
`gmt_create` DATETIME ,
`gmt_modified` DATETIME,
PRIMARY KEY(`row_key`)
);
CREATE TABLE `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
store {
mode = "db"
db {
datasource = "druid"
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:13306/test1?rewriteBatchedStatements=true"
user = "root"
password = "admin123"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
## 这个位置就是自定义事务组
service{
vgroupMapping.my_test_tx_group = "xu_shiwu"
default.grouplist = "127.0.0.1:8091"
disableGlobalTransaction = false
disable = false
}
步骤四:修改registry.conf配置文件,选择注册中心为nacos,设置连接信息
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
步骤五:进入bin目录启动双击启动,进入nacos检查是否启动成功
步骤六:naocs新增配置文件,分组和registry.conf配置文件里的保持一致,名称为service.vgroupMapping. + 自己起的事务组名称,我在这里就是xu_shiwu,选择test设置值为default。如果不配置,项目启动时就会看到这个ERROR哦
2022-08-21 16:32:40.492 INFO 12932 --- [ main] c.a.nacos.client.config.impl.CacheData : [fixed-127.0.0.1_8848] [add-listener] ok, tenant=, dataId=service.vgroupMapping.xu_shiwu, group=SEATA_GROUP, cnt=1
2022-08-21 16:32:40.709 ERROR 12932 --- [ main] i.s.c.r.netty.NettyClientChannelManager : can not get cluster name in registry config 'service.vgroupMapping.xu_shiwu', please make sure registry config correct
2022-08-21 16:32:40.802 INFO 12932 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
二、依赖列表
这是我用的完整依赖(生产者与消费者用的都一样的),使用的JPA操作数据库,可以换成mybatis,效果都是一样的
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
三、代码编写
3.1、共用配置
写这块时候代码有点乱了,再加上自己调来调去的,代码结构比价混乱,
所以截图看一下我的代码结构吧,下边会给上所有代码,如果有照着以前写代码的小伙伴,自己按需粘贴 注意resource目录,把seata的俩文件粘贴进来了(是改完后的,不是刚解压那会的)
两边的实体类都是一样的,get和set方法别忘了加上,如果和我一样用的是 JPA,表会自动创建的
@Entity
@Table(name = "users")
@DynamicUpdate
public class Users implements Serializable {
@Id
//@GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long uid;
@Column(columnDefinition = "varchar(50) comment '姓名'")
private String name;
@Column(columnDefinition = "varchar(50) comment '密码'")
private String pwd;
@Column(name = "log_id")
private Long LogId;
}
@Entity
@Table(name = "users_log")
@DynamicUpdate
public class UsersLog implements Serializable {
@Id
//@GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long logId;
private String log;
}
配置文件只需要把端口号,项目名称改了就好,数据源改了,seata的事务名称是自己在配置文件里写的
server:
port: 8201
spring:
profiles:
active: test
application:
name: user01
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:13306/test1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
jpa:
generate-ddl: false
database-platform: org.hibernate.dialect.MySQL5Dialect
show-sql: true
hibernate:
ddl-auto: update
properties:
use_sql_comments: false
format_sql: false
seata:
enabled: true
enable-auto-data-source-proxy: true
# 这个tx-service-group 跟配置文件的一样,
tx-service-group: xu_shiwu
config:
type: nacos
nacos:
serverAddr: 127.0.0.1:8848
group: SEATA_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace:
username: nacos
password: nacos
service:
vgroup-mapping:
# 这个my_test_tx_group 是配置文件里自定的
my_test_tx_group: xu_shiwu
disable-global-transaction: false
client:
rm:
report-success-enable: false
3.2、服务提供者代码编写
这里是把user01项目当服务提供者使用的,代码如下
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
// 接口
public interface UserService {
public String info(@RequestParam("name") String name, @RequestParam("pwd") String pwd);
public String findOne(@PathVariable("id") String id);
public Users insert(Users users);
}
// 接口实现类
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UsersRespository usersRespository;
@Override
public String info(String name, String pwd) {
return null;
}
@Override
public String findOne(String id) {
Optional<Users> byId = usersRespository.findById(Long.parseLong(id));
try {
return byId.get().getName();
}catch (Exception e){
return "未找到用户";
}
}
@Override
public Users insert(Users users) {
Users save = usersRespository.save(users);
return save;
}
}
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("user/info")
public String info(@RequestParam("name") String name, @RequestParam("pwd") String pwd){
return userService.info(name, pwd);
}
@GetMapping("user/findOne/{id}")
public String findOne(@PathVariable("id") String id) {
return userService.findOne(id);
}
@PostMapping("user/insert")
public Users insert(@RequestBody Users users) {
return userService.insert(users);
}
}
/**
* 这个是JPA的接口,所以没有给定义方法
*/
public interface UsersRespository extends JpaRepository<Users, Long>, JpaSpecificationExecutor<Users> {
}
3.3、服务调用者代码编写
这里是把provider01项目当服务提供者使用的,代码如下
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@RestController
@RequestMapping("/userLog")
public class UsersLogController {
@Autowired
private UsersLogService usersLogService;
@Autowired
private UserService userService;
@RequestMapping("/findUserId")
public String findUserById(){
return userService.findOne("1");
}
@RequestMapping("/test1")
public UsersLog insert(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
UsersLog usersLog = new UsersLog();
usersLog.setLog("日志记录" + simpleDateFormat.format(new Date()));
return usersLogService.insert(usersLog);
}
@RequestMapping("/test2")
public String insertAndUser(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
UsersLog usersLog = new UsersLog();
usersLog.setLog("日志记录" + simpleDateFormat.format(new Date()));
Users users = new Users();
users.setName("张三");
users.setPwd("1223333");
return usersLogService.insertAndUser(usersLog, users);
}
}
/**
* 这是JPA接口
*/
public interface UserLogRespository extends JpaRepository<UsersLog, Long>, JpaSpecificationExecutor<UsersLog> {
}
/** 远程调用users的接口 */
@Component
@FeignClient(name="user01")
public interface UserService {
@GetMapping("user/info")
public String info(@RequestParam("name") String name, @RequestParam("pwd") String pwd);
@GetMapping("user/findOne/{id}")
public String findOne(@PathVariable("id") String id);
@PostMapping("user/insert")
public Users insert(@RequestBody Users users);
}
/**
* 当前项目自身接口与实现类
*/
public interface UsersLogService {
public UsersLog insert(UsersLog usersLog);
public String insertAndUser(UsersLog usersLog, Users users);
}
@Service
public class UsersLogServiceImpl implements UsersLogService{
@Autowired
private UserLogRespository userLogRespository;
@Autowired
private UserService userService;
public UsersLog insert(UsersLog usersLog){
return userLogRespository.save(usersLog);
}
@GlobalTransactional(name = "xu_shiwu")
public String insertAndUser(UsersLog usersLog, Users users){
userLogRespository.save(usersLog);
//int i = 1 / 0;
userService.insert(users);
//int x = 2 / 0;
return "success";
}
}
3.4、测试方式
我这里设置provider01项目端口为8101,分别调用了三个接口
测试项目远程调用是否正常:http://localhost:8101/userLog/findUserId
测试当前项目JPA能否保存数据:http://localhost:8101/userLog/test1
用于事务测试:http://localhost:8101/userLog/test2
结论:当把 @GlobalTransactional(name = “xu_shiwu”)注释掉,并执行int i = 1 / 0代码,UserLog表会成功添加数据,而user表无数据废话,远程接口都没调用。当把注解打开,UsersLog表数据会回滚成功,然而并不能证明user表那边事务也会回滚!!!
解除int x = 2 / 0,如果两张表都没有数据,则表示分布式事务回滚成功!!!