eureka {
serviceUrl = “http://localhost:8761/eureka”
application = “default”
weight = “1”
}
redis {
serverAddr = “localhost:6379”
db = 0
password = “”
cluster = “default”
timeout = 0
}
zk {
cluster = “default”
serverAddr = “127.0.0.1:2181”
sessionTimeout = 6000
connectTimeout = 2000
username = “”
password = “”
}
consul {
cluster = “default”
serverAddr = “127.0.0.1:8500”
}
etcd3 {
cluster = “default”
serverAddr = “http://localhost:2379”
}
sofa {
serverAddr = “127.0.0.1:9603”
application = “default”
region = “DEFAULT_ZONE”
datacenter = “DefaultDataCenter”
cluster = “default”
group = “SEATA_GROUP”
addressWaitTime = “3000”
}
file {
name = “file.conf”
}
}
config {
file、nacos 、apollo、zk、consul、etcd3
使用nacos管理配置
type = “nacos”
nacos {
nacos ip
serverAddr = “127.0.0.1:8848”
所在命名空间
namespace = “7e3699fa-09eb-4d47-8967-60f6c98da94a”
所在分组
group = “EXAMPLE-GROUP”
username = “nacos”
password = “nacos”
}
consul {
serverAddr = “127.0.0.1:8500”
}
apollo {
appId = “seata-server”
apolloMeta = “http://192.168.1.204:8801”
namespace = “application”
}
zk {
serverAddr = “127.0.0.1:2181”
sessionTimeout = 6000
connectTimeout = 2000
username = “”
password = “”
}
etcd3 {
serverAddr = “http://localhost:2379”
}
file {
name = “file.conf”
}
}
以上内容主要修改了注册中心与配置中心为Nacos并且修改了Nacos地址与登录账号/登录密码,命名空间,分组;
配置部署到Nacos
==========
这里简化了下Nacos官网下载的config.txt内容,从官网下载的配置文本以下内容标记需要修改的需要关注
Copy#事务组 重点关注
service.vgroupMapping.my_test_tx_group=default
#服务段分组地址
service.default.grouplist=127.0.0.1:8091
#保持默认
service.enableDegrade=false
#保持默认
service.disableGlobalTransaction=false
#存储方式选择 db模式则数据库
store.mode=db
#需修改
store.lock.mode=db
#需修改
store.session.mode=db
store.publicKey=
#需修改
store.db.datasource=druid
#需修改
store.db.dbType=mysql
#需修改
store.db.driverClassName=com.mysql.jdbc.Driver
#需修改
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
#需修改
store.db.user=root
#需修改
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
client.undo.dataValidation=true
#需修改
#jackson改为kryo 解决数据库Datetime类型问题
client.undo.logSerialization=kryo
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
其中该配置需要重点关注service.vgroupMapping.my_test_tx_group=default这里的配置与微服务应用中的配置必须要一致后面会描述到。
由于有时间类型是Seata回滚反序列化Date类型无法成功反序列化,需要修改序列化方式解决该问题: client.undo.logSerialization=kryo
修改完所有配置运行从官网下载的nacos-config.sh文件将文本内容上次到nacos配置中心中:
Copy# -h ip -p 端口 -t 命名空间 -g 分组
sh nacos-config.sh -h localhost -p 8848 -t 7e3699fa-09eb-4d47-8967-60f6c98da94a -g EXAMPLE-GROUP
部署好配置文件之后在Nacos命名空间为7e3699fa-09eb-4d47-8967-60f6c98da94a(dev)的配置管理界面可以看到文本中的内容。
Seata数据库
========
按照config.txt中对应的数据库连接信息创建Seata数据库并且创建以下几张表
CopyCREATE TABLE IF NOT EXISTS 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
)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
– the table to store BranchSession data
CREATE TABLE IF NOT EXISTS branch_table
(
branch_id
BIGINT NOT NULL,
xid
VARCHAR(128) NOT NULL,
transaction_id
BIGINT,
resource_group_id
VARCHAR(32),
resource_id
VARCHAR(256),
branch_type
VARCHAR(8),
status
TINYINT,
client_id
VARCHAR(64),
application_data
VARCHAR(2000),
gmt_create
DATETIME(6),
gmt_modified
DATETIME(6),
PRIMARY KEY (branch_id
),
KEY idx_xid
(xid
)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
– the table to store lock data
CREATE TABLE IF NOT EXISTS lock_table
(
row_key
VARCHAR(128) NOT NULL,
xid
VARCHAR(96),
transaction_id
BIGINT,
branch_id
BIGINT NOT NULL,
resource_id
VARCHAR(256),
table_name
VARCHAR(32),
pk
VARCHAR(36),
gmt_create
DATETIME,
gmt_modified
DATETIME,
PRIMARY KEY (row_key
),
KEY idx_branch_id
(branch_id
)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
部署Seata Server
==============
以上工作准备就绪,进入bin目录运行seata-server.bat(windows用户)/seata-server.sh(linux用户)即可。
Seata应用场景模拟#
============
这里做一个用户服务用户登录成功后调用会员服务增加会员积分场景案例。
父工程改造
=====
工程名称:spring-cloud-alibaba-version-parent,增加mybatis,seata序列化等依赖版本管理。
Copy
<mybatis.plus.version>3.4.2</mybatis.plus.version>
<mybatis.plus.ds.version>2.5.4</mybatis.plus.ds.version>
<seata.serializer.kryo.version>1.3.0</seata.serializer.kryo.version>
com.baomidou
mybatis-plus-boot-starter
${mybatis.plus.version}
io.seata
seata-serializer-kryo
${seata.serializer.kryo.version}
会员服务工程改造
========
工程名称:spring-cloud-alibaba-service-member,增加数据库与Seata依赖,增加用户会员积分接口。
pom.xml
Copy
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
io.seata
seata-serializer-kryo
com.baomidou
mybatis-plus-boot-starter
mysql
mysql-connector-java
bootstrap.yaml
Copy#注意,此处省略之前配置的信息…
#注意,此处省略之前配置的信息…
#注意,此处省略之前配置的信息…
#注意,此处省略之前配置的信息…
#数据库信息配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/member_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
#Seata配置
seata:
enabled: true
application-id: ${spring.application.name}
#对应nacos配置 service.vgroupMapping.my_test_tx_group
tx-service-group: ‘my_test_tx_group’
service:
vgroup-mapping:
#对应nacos配置 service.vgroupMapping.my_test_tx_group 的值 default
my_test_tx_group: ‘default’
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
#cluster: ${spring.cloud.nacos.discovery.cluster}
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
注意事项:
-
bootstrap.yaml中seata.tx-service-group 配置项一定要配置nacos配置中心中service.vgroupMapping对应的my_test_tx_group。也就是说一定要保持一致。
-
bootstrap.yaml中seata.service.vgroup-mapping.my_test_tx_group配置项一定要配置nacos配置中心对应service.vgroupMapping.my_test_tx_group配置祥的值。
如果没有注意上方两点将会导致启动时报:no available service ‘default’ found, please make sure registry config correct。
创建member_db数据库
其中undo_log表为Seata回滚日志表,需要在每个使用到Seata的业务服务数据库中都需要创建。
CopySET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
– Table structure for t_member_integral
DROP TABLE IF EXISTS t_member_integral
;
CREATE TABLE t_member_integral
(
ID
bigint(20) NOT NULL COMMENT ‘主键’,
USERNAME
varchar(55) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ‘用户名称’,
INTEGRAL
int(11) DEFAULT NULL COMMENT ‘积分’,
CREDATE
datetime(0) DEFAULT NULL COMMENT ‘时间’,
PRIMARY KEY (ID
) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
– Table structure for undo_log
DROP TABLE IF EXISTS undo_log
;
CREATE TABLE undo_log
(
id
bigint(20) NOT NULL AUTO_INCREMENT,
branch_id
bigint(20) NOT NULL,
xid
varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
context
varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
rollback_info
longblob NOT NULL,
log_status
int(11) NOT NULL,
log_created
datetime(0) NOT NULL,
log_modified
datetime(0) NOT NULL,
ext
varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (id
) USING BTREE,
UNIQUE INDEX ux_undo_log
(xid
, branch_id
) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
新增会员积分CRUD
我这里新增以下类,具体内容大家都比较熟悉。
CopyMemberIntegralController.java
IMemberIntegralBiz.java
IMemberIntegralBizImpl.java
MemberIntegralMapper.java
MemberIntegral.xml
在这里所有增加会员积分的逻辑都写在同一个类中 MemberIntegralController.java
Copyimport com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.gitee.eample.member.service.biz.IMemberIntegralBiz;
import com.gitee.eample.member.service.domain.MemberIntegral;
import com.gtiee.example.common.exception.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
-
用户积分
-
@author wentao.wu
*/
@RestController
@RequestMapping(“/member/integral”)
public class MemberIntegralController {
@Autowired
private IMemberIntegralBiz memberIntegralBiz;
@PostMapping(“/login/{username}”)
public Response login(@PathVariable(“username”) String username) {
// 每天第一次登录则增加积分,我这里就不判断了,每次调用都新增一条积分记录了
MemberIntegral memberIntegral = new MemberIntegral();
memberIntegral.setId(IdWorker.getId());
memberIntegral.setIntegral(10);//固定10积分
memberIntegral.setUsername(username);
memberIntegral.setCredate(new Date());
memberIntegralBiz.save(memberIntegral);
return Response.createOk(“登录新增会员积分成功!”, true);
}
}
运行MemberServiceApplication.java启动服务,如果想知道有没有注册成功:
第一可以看Seata Server端有没有日志输出,该日志内容主要为注册的业务服务的数据库信息。
第二可以看业务服务有没有输出以下日志,有输出以下日志则Seata Server端注册成功
Copy2021-11-05 09:56:30.962 INFO 16420 — [ main] i.s.c.r.netty.NettyClientChannelManager : will connect to 2.0.4.58:8091
2021-11-05 09:56:30.962 INFO 16420 — [ main] i.s.c.rpc.netty.RmNettyRemotingClient : RM will register :jdbc:mysql://localhost:3306/member_db
2021-11-05 09:56:30.967 INFO 16420 — [ main] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:2.0.4.58:8091,msg:< RegisterRMRequest{resourceIds=‘jdbc:mysql://localhost:3306/member_db’, applicationId=‘service-member’, transactionServiceGroup=‘my_test_tx_group’} >
用户服务工程改造
========
工程名称:spring-cloud-alibaba-service-member,增加数据库与Seata依赖,增加用户登录接口,增加调用会员服务积分接口feign。
由于内容一致此处省略pom.xml,bootstrap.xml(里面注意数据库要修改为用户服务的数据库)。
创建user_db数据库
其中undo_log表为Seata回滚日志表,需要在每个使用到Seata的业务服务数据库中都需要创建。
Copy
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
– Table structure for t_user
DROP TABLE IF EXISTS t_user
;
CREATE TABLE t_user
(
ID
bigint(20) NOT NULL COMMENT ‘主键’,
USERNAME
varchar(55) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ‘用户名’,
PWD
varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ‘密码’,
ADDR
varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ‘地址’,
LAST_LOGIN_DATE
datetime(0) DEFAULT NULL COMMENT ‘最后登录时间’,
PRIMARY KEY (ID
) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
– Records of t_user
INSERT INTO t_user
VALUES (1, ‘test1’, ‘123456’, ‘123’, NULL);
– Table structure for undo_log
DROP TABLE IF EXISTS undo_log
;
CREATE TABLE undo_log
(
id
bigint(20) NOT NULL AUTO_INCREMENT,
branch_id
bigint(20) NOT NULL,
xid
varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
context
varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
rollback_info
longblob NOT NULL,
log_status
int(11) NOT NULL,
log_created
datetime(0) NOT NULL,
log_modified
datetime(0) NOT NULL,
ext
varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (id
) USING BTREE,
UNIQUE INDEX ux_undo_log
(xid
, branch_id
) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
新增用户登录CRUD
我这里新增以下类,具体内容大家都比较熟悉。
CopyUserController.java
IUserBiz.java
IUserBizImpl.java
UserMapper.java
UserMapper.xml
MemberInfoControllerClient.java
MemberInfoControllerClient.java
Copy/**
-
service-member服务远程调用接口
-
@author wentao.wu
*/
@FeignClient(name = “service-member”)
public interface MemberInfoControllerClient {
/**
-
登录送积分
-
@param username
-
@return
*/
@PostMapping(“/member/integral/login/{username}”)
Response login(@PathVariable(“username”)String username);
}
IUserBiz.java
Copypublic interface IUserBiz extends IService {
/**
-
用户登录并且赠送第一次登录积分
-
@param command
-
@return
*/
boolean login(UserLoginCommand command);
}
IUserBizImpl.java
Copypackage com.gitee.eample.user.service.biz;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gitee.eample.user.service.controller.command.UserLoginCommand;
import com.gitee.eample.user.service.dao.UserMapper;
import com.gitee.eample.user.service.domain.User;
import com.gitee.eample.user.service.feign.MemberInfoControllerClient;
import com.gtiee.example.common.exception.Response;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.Date;
@Service
public class IUserBizImpl extends ServiceImpl<UserMapper, User> implements IUserBiz {
@Autowired
private MemberInfoControllerClient client;
@GlobalTransactional(name = “login_add_member_intergral”,rollbackFor = Exception.class)//开启分布式事务
@Override
public boolean login(UserLoginCommand command) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, command.getUsername())
.eq(User::getPwd, command.getPwd());
User loginUser = getOne(wrapper);
if (ObjectUtils.isEmpty(loginUser)) {
return false;
}
//调用会员登录接口增加积分
Response response = client.login(command.getUsername());
if (response.isOk()) {//增加积分成功,或已增加积分
//调用积分接口成功,修改当前用户登录时间
loginUser.setLastLoginDate(new Date());
updateById(loginUser);
//假设此处发生异常,不但修改当前用户登录时间需要回滚并且新增的会员积分信息也回滚才算正常
int i = 0 / 0;
return true;
} else {
//增加积分失败
return false;
}
}
}
UserController.java
Copyimport com.gitee.eample.user.service.biz.IUserBiz;
import com.gitee.eample.user.service.controller.command.UserLoginCommand;
import com.gtiee.example.common.exception.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
-
User Business Controller
-
@author wentao.wu
*/
@RestController
@RequestMapping(“/users/”)
public class UserController {
private Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
private IUserBiz userBiz;
@PostMapping(“/login”)
public Response login(UserLoginCommand command) {
try {
boolean result = userBiz.login(command);
if (result) {
return Response.createOk(“登录并赠送积分成功!”, result);
}else{
return Response.createError(“账号或密码不存在!”, result);
}
} catch (Exception e) {
logger.error(“登录失败!”, e);
return Response.createError(“服务器繁忙请稍后再试!”, false);
}
}
}