目录
三、springcloud03
3.1 普通事务
普通事务
1、ACID事务的特点:原子性、一致性、隔离性、持久性
2、事务并发带来的问题:脏读、幻读、不可重复读、丢失修改
3、我们可以使用事务的隔离级别来解决
读未提交、读已提交、可重复读、串行化
事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列SQL操作,这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行。
3.2 演示事务
3.2.1、建库
创建三个数据库,每个数据库一张表,表名和库名一致即可,这里使用的是mysql5.7的建表语句,没有collect
/*
Navicat Premium Data Transfer
Source Server : localhost_3306
Source Server Type : MySQL
Source Server Version : 80011
Source Host : localhost:3306
Source Schema : seata_account
Target Server Type : MySQL
Target Server Version : 80011
File Encoding : 65001
Date: 28/11/2022 15:28:32
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_account
-- ----------------------------
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NULL DEFAULT NULL,
`total` int(11) NULL DEFAULT NULL,
`used` int(11) NULL DEFAULT NULL,
`residue` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4;
-- ----------------------------
-- Records of t_account
-- ----------------------------
INSERT INTO `t_account` VALUES (1, 1, 1000, 100, 900);
SET FOREIGN_KEY_CHECKS = 1;
/*
Navicat Premium Data Transfer
Source Server : localhost_3306
Source Server Type : MySQL
Source Server Version : 80011
Source Host : localhost:3306
Source Schema : seata_order
Target Server Type : MySQL
Target Server Version : 80011
File Encoding : 65001
Date: 28/11/2022 15:28:38
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NULL DEFAULT NULL,
`product_id` int(11) NULL DEFAULT NULL,
`count` int(11) NULL DEFAULT NULL,
`money` bigint(20) NULL DEFAULT NULL,
`status` int(11) NULL DEFAULT 0 COMMENT '0 未支付 1 已支付',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4;
-- ----------------------------
-- Records of t_order
-- ----------------------------
INSERT INTO `t_order` VALUES (47, 1, 1, 5, 100, 1);
SET FOREIGN_KEY_CHECKS = 1;
/*
Navicat Premium Data Transfer
Source Server : localhost_3306
Source Server Type : MySQL
Source Server Version : 80011
Source Host : localhost:3306
Source Schema : seata_storage
Target Server Type : MySQL
Target Server Version : 80011
File Encoding : 65001
Date: 28/11/2022 15:28:45
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_storage
-- ----------------------------
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NULL DEFAULT NULL,
`total` int(11) NULL DEFAULT NULL COMMENT '总量',
`used` int(11) NULL DEFAULT NULL COMMENT '已经使用的数量',
`residue` int(11) NULL DEFAULT NULL COMMENT '剩余的数据量',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8;
-- ----------------------------
-- Records of t_storage
-- ----------------------------
INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);
SET FOREIGN_KEY_CHECKS = 1;
3.2.2 创建父工程
删掉src,只保留pom文件即可
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ykq</groupId>
<artifactId>seata_parent</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>seata-common</module>
<module>seata-account</module>
<module>seata-order</module>
<module>seata-storage</module>
</modules>
<packaging>pom</packaging>
<!--引入父工程-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
3.2.3 编写公共微服务
1、pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata_parent</artifactId>
<groupId>com.ykq</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-common</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
2、实体类
package com.ykq.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
//账户
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private int id;
private int userId;
private int total;
private int used;
private int residue;
}
package com.ykq.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
//统一返回结果
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this(code, message, null);
}
}
package com.ykq.entity;
import lombok.Data;
//订单
@Data
public class Order {
private int id;
private int userId;
private int productId;
private double money;
private int count;
private int status;
}
package com.ykq.entity;
import lombok.Data;
//库存
@Data
public class Storage {
private int id;
private int productId;
private int total;
private int used;//被用了多少个
private int residue;//剩余的个数。
}
3.2.4 编写账户微服务
1、pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata_parent</artifactId>
<groupId>com.ykq</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-account</artifactId>
<dependencies>
<!--引入公共依赖-->
<dependency>
<groupId>com.ykq</groupId>
<artifactId>seata-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--nacos注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--openfeign:用于服务间远程调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<!--排除其中的某个版本,可以再在外面添加自己需要的版本-->
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
2、配置文件
server:
port: 8002
spring:
# 数据源配置
datasource:
url: jdbc:mysql://localhost:3306/seata_account?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 密码
#配置服务名
application:
name: seata-account
#nacos注册中心的地址
cloud:
nacos:
discovery:
server-addr: localhost:8848
mybatis:
mapperLocations: classpath:mapper/*.xml
logging:
level:
com.ykq.dao: debug
3、mapper文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ykq.dao.AccountDao">
<resultMap id="account" type="com.ykq.entity.Account">
<result property="id" column="id" jdbcType="BIGINT"/>
<result property="userId" column="user_id" jdbcType="BIGINT"/>
<result property="total" column="total" jdbcType="DECIMAL"/>
<result property="used" column="used" jdbcType="DECIMAL"/>
<result property="residue" column="residue" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
update t_account set used = used + #{money},residue = residue - #{money}
where user_id=#{userId}
</update>
</mapper>
4、mapper接口
package com.ykq.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
@Mapper
public interface AccountDao {
void decrease(@Param("userId") int userId, @Param("money") double money);
}
5、service接口
package com.ykq.service;
public interface AccountService {
void decrease(int userId, double money);
}
6、service实现层
package com.ykq.service.impl;
import com.ykq.dao.AccountDao;
import com.ykq.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void decrease(int userId, double money) {
accountDao.decrease(userId,money);
}
}
7、controller层
package com.ykq.controller;
import com.ykq.entity.CommonResult;
import com.ykq.service.AccountService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.math.BigDecimal;
@RestController
@RequestMapping(value = "account")
public class AccountController {
@Resource
private AccountService accountService;
/**
* 根据用户id扣余额
* @param userId
* @param money
* @return
*/
@PostMapping("/increase/{userId}/{money}")
public CommonResult increase(@PathVariable("userId")Integer userId,@PathVariable("money")double money) {
accountService.decrease(userId, money);
return new CommonResult(200,"账户余额扣减成功,哈哈哈");
}
}
8、启动类
package com.ykq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
//1.3
@SpringBootApplication
@EnableDiscoveryClient
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
3.2.5 编写库存微服务
1、pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata_parent</artifactId>
<groupId>com.ykq</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-storage</artifactId>
<dependencies>
<dependency>
<groupId>com.ykq</groupId>
<artifactId>seata-common</artifactId>
<version>1.0-SNAPSHOT</version>
</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.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
和账户微服务的pom文件差不多,可以参考账户微服务
2、配置文件
server:
port: 8003
spring:
# 数据源配置
datasource:
url: jdbc:mysql://localhost:3306/seata_storage?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 密码
application:
name: seata-storage
cloud:
nacos:
discovery:
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: fsp_tx_group
mybatis:
mapperLocations: classpath:mapper/*.xml
logging:
level:
com.ykq.dao: debug
3、mapper文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ykq.dao.StorageDao">
<resultMap id="order" type="com.ykq.entity.Storage">
<result property="id" column="id" jdbcType="BIGINT"/>
<result property="product_id" column="productId" jdbcType="BIGINT"/>
<result property="total" column="total" jdbcType="INTEGER"/>
<result property="used" column="used" jdbcType="INTEGER"/>
<result property="residue" column="residue" jdbcType="INTEGER"/>
</resultMap>
<update id="decrease">
update t_storage set used = used + #{count},residue = residue -#{count}
where product_id = #{productId}
</update>
</mapper>
4、mapper接口
package com.ykq.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
public interface StorageDao {
void decrease(@Param("productId") int productId,@Param("count") Integer count);
}
5、service接口
package com.ykq.service;
public interface StorageService {
void decrease(int productId, Integer count);
}
6、service实现类
package com.ykq.service.impl;
import com.ykq.dao.StorageDao;
import com.ykq.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageDao storageDao;
@Override
@Transactional
public void decrease(int productId, Integer count) {
log.info("库存扣减开始----");
storageDao.decrease(productId,count);
log.info("库存扣减结束----");
}
}
7、controller层
package com.ykq.controller;
import com.ykq.entity.CommonResult;
import com.ykq.service.StorageService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping(value = "/storage")
public class StorageController {
@Resource
private StorageService storageService;
/**
* 根据商品id扣减库存
* @param productId
* @param count
* @return
*/
@PostMapping("/increase/{productId}/{count}")
CommonResult increase(@PathVariable("productId") int productId, @PathVariable("count") int count){
storageService.decrease(productId, count);
return new CommonResult(200,"库存扣减成功,哈哈哈哈");
}
}
8、启动类
package com.ykq;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@MapperScan("com.ykq.dao")
public class StorageApplication {
public static void main(String[] args) {
SpringApplication.run(StorageApplication.class, args);
}
}
3.2.6 编写订单微服务
1、pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata_parent</artifactId>
<groupId>com.ykq</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order</artifactId>
<dependencies>
<dependency>
<groupId>com.ykq</groupId>
<artifactId>seata-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--阿里巴巴的注册中心nacos-->
<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>
<!--springbootweb项目-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring连接数据库的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--springboot和mybatis整合的依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--openfeign的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--热部署依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--mysql的驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--为了让你的application.yml文件有提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--spring单元测试的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
2、配置文件
server:
port: 8001
spring:
application:
name: seata-order
# 数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
username: root
password: 密码
# 把该服务注册到nacos上
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 配置mybatis映射文件的路径
mybatis:
mapperLocations: classpath:mapper/*.xml
# 显示mybatis执行的sql语句
logging:
level:
com.ykq.dao: debug
3、mapper文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ykq.dao.OrderDao">
<resultMap id="order" type="com.ykq.entity.Order">
<result property="id" column="id" jdbcType="BIGINT"/>
<result property="user_id" column="userId" jdbcType="BIGINT"/>
<result property="product_id" column="productId" jdbcType="BIGINT"/>
<result property="count" column="count" jdbcType="INTEGER"/>
<result property="money" column="money" jdbcType="BIGINT"/>
<result property="status" column="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="saveOrder" useGeneratedKeys="true" keyProperty="id">
insert into t_order(user_id,product_id,count,money,status)
value (#{userId},#{productId},#{count},#{money},0)
</insert>
<update id="updateStatus">
update t_order set status = 1
where id = #{id} and status = 0
</update>
</mapper>
4、mapper接口
package com.ykq.dao;
import com.ykq.entity.Order;
public interface OrderDao {
void saveOrder(Order order);
void updateStatus(int id);
}
5、feign接口
因为要实现远程调用,所以这里要编写feign接口
package com.ykq.feign;
import com.ykq.entity.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@FeignClient(value="seata-account")
public interface AccountFeign {
@PostMapping("/account/increase/{userId}/{money}")
public CommonResult increase(@PathVariable("userId")Integer userid, @PathVariable("money")double money);
}
package com.ykq.feign;
import com.ykq.entity.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@FeignClient("seata-storage")
public interface StorageFeign {
@PostMapping("/storage/increase/{productId}/{count}")
CommonResult increase(@PathVariable("productId") int productId, @PathVariable("count") int count);
}
6、service接口
package com.ykq.service;
import com.ykq.entity.Order;
public interface OrderService {
void saveOrder(Order order);
}
7、service实现类
package com.ykq.service.impl;
import com.ykq.dao.OrderDao;
import com.ykq.entity.Order;
import com.ykq.feign.AccountFeign;
import com.ykq.feign.StorageFeign;
import com.ykq.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private AccountFeign accountFeign;//表示调用的远程服务。
@Autowired
private StorageFeign storageFeign;
@Override
@Transactional
public void saveOrder(Order order) {
log.info("-------->开始创建新订单");
orderDao.saveOrder(order);
log.info("-------订单微服务开始调用账户,做扣减");
accountFeign.increase(order.getUserId(),order.getMoney()); //当前的事务回滚只会回滚自己的数据
log.info("-------订单微服务开始调用账户,做扣减end");
log.info("--------订单微服务开始调用库存,做扣减");
storageFeign.increase(order.getProductId(),order.getCount());
log.info("-------订单微服务开始调用库存,做扣减end");
log.info("-------修改订单状态");
orderDao.updateStatus(order.getId());
log.info("-------修改订单状态结束");
log.info("--------下订单结束了,哈哈哈哈");
}
}
8、controller层
package com.ykq.controller;
import com.ykq.entity.CommonResult;
import com.ykq.entity.Order;
import com.ykq.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("addOrder")
public CommonResult addOrder(Order order){
try {
orderService.saveOrder(order);
return new CommonResult(2000, "下单成功");
}catch (Exception e){
return new CommonResult(5000, "下单失败");
}
}
}
9、启动类
package com.ykq;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication //排除原有的数据源的配置
@MapperScan("com.ykq.dao")
@EnableFeignClients //开启feign的注解
@EnableTransactionManagement
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class,args);
}
}
3.2.7 测试
代码编写完成,如果是粘贴的整个项目,配置文件红maven仓库也需要修改为自己的
3.2.7.1 成功测试
1、数据库初始状态
2、浏览器访问订单资源进行测试
请求为:http://localhost:8001/order/addOrder/?userId=1&productId=1&count=5&money=100
响应结果为:{"code":2000,"message":"下单成功","data":null}
3、响应成功后数据库
3.2.7.2 异常测试
人为的给订单微服务程序添加一个异常情况 【算术异常即可】比如:10/0
1、修改订单的service实现类
package com.ykq.service.impl;
import com.ykq.dao.OrderDao;
import com.ykq.entity.Order;
import com.ykq.feign.AccountFeign;
import com.ykq.feign.StorageFeign;
import com.ykq.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private AccountFeign accountFeign;//表示调用的远程服务。
@Autowired
private StorageFeign storageFeign;
@Override
@Transactional
public void saveOrder(Order order) {
log.info("-------->开始创建新订单");
orderDao.saveOrder(order);
log.info("-------订单微服务开始调用账户,做扣减");
accountFeign.increase(order.getUserId(),order.getMoney()); //当前的事务回滚只会回滚自己的数据
log.info("-------订单微服务开始调用账户,做扣减end");
//添加异常
int c=10/0;
log.info("--------订单微服务开始调用库存,做扣减");
storageFeign.increase(order.getProductId(),order.getCount());
log.info("-------订单微服务开始调用库存,做扣减end");
log.info("-------修改订单状态");
orderDao.updateStatus(order.getId());
log.info("-------修改订单状态结束");
log.info("--------下订单结束了,哈哈哈哈");
}
}
注:这里异常添加在账户微服务之后,订单微服务之前
2、再次进行测试
请求:http://localhost:8001/order/addOrder/?userId=1&productId=1&count=5&money=100
请求结果:{"code":5000,"message":"下单失败","data":null}
3、查看数据库状态
3.3 分布式事务
分布式服务结构图如下:
因为每个事务都连接了自己的数据库,所以在进行事务操作时,
一个微服务(A)的成功,就会改变自己的数据库,即使其它微服务(B)失败,该微服务(A)也不会回滚自己的数据库
1. 借助rabbitMQ消息中间件 2. 可以自己编写代码完成分布式事务。 3. 借助于第三方分布式事务服务器。---阿里巴巴seata
3.4 介绍seata
seata官网地址:Seata部署指南
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT[两次提交]、TCC、SAGA 和XA 事务模式,为用户打造一站式的分布式解决方案。
这次主要练习的就是AT事务模式
Seata的执行流程如下:
1、A服务【订单微服务】的TM[事务发起者]向TC[seata服务端]申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID
2、A服务开始远程调用B服务【账户微服务】,此时XID会在微服务的调用链上传播
3、B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖
4、B服务执行分支事务,向数据库做操作
5、全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚
6、TC协调其管辖之下的所有分支事务, 决定是否回滚
根据上面的测试进行举例来看,每一部分的角色如下
TM:事务的发起者--下单
RM: 连接数据库--微服务
TC:全局事务管理者--seata服务器
XID:全局事务id
3.5 搭建seata服务器
seata官网:Release v1.3.0 · seata/seata · GitHub
seata在1.3.0之后支持集群模式
解压文件
3.5.1 解决集群共享数据
1、修改conf/file.conf文件,更改数据保存的位置
1.1将源码压缩包解压后,找到其中script文件夹,将其复制到seata文件目录下
1.2 找到script\server\db目录下的mysql.sql文件,就是我们需要的三张表,将表添加到mysql中的seata数据库
1.3 因为在设置数据库时,我们选择的驱动是8的驱动,所以我们也需要有8的jar包,默认读取的是lib文件目录下的jar包,所以可以将seata/lib/jdbc/目录下的jar包粘贴到seata/lib目录下
3.5.2 seata连接nacos
当seata是一个集群时,微服务连接seata会无法选择seata,将seata接入到nacos,别的微服务就可以在nacos中拉取对应的seata服务
1、找到seata/conf/registry.conf文件进行修改
2、上传配置文件到nacos配置中心
找到seata\script\config-center目录下的config.txt文件,修改其中的配置,修改完之后,这些配置是需要放到配置中心的
注!!!!!!!!!!!!!!
url那一行,在最后多了一个双引号,一定要删掉
然后使用脚本将这些修改好的配置文件,传到nacos注册中心,脚本位置在seata\script\config-center\nacos\nacos-config.sh,如果安装了git,那么这里双击此脚本,会自动调用git的控制台,执行命令,将配置上传到配置中心
再执行时,也可以指定nacos的信息
3、启动搭建好的seata服务器,他会自动注册到nacos中
找到seata\bin目录下的seata-server.bat文件,双击即可执行,
如果需要指定端口号,可以进入到cmd中使用命令执行
seata-server.bat -Dserver.port=8989
3.6 配置微服务客户端
3.6.1 创建数据库
在每一个微服务的数据库中建一张日志表,表结构在seata\script\client\at\db目录下的mysql.sql中
3.6.2 修改微服务代码
这里先使用storage模块进行测试
1、添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!--如果seata的版本不对,可以在这里先排除一个版本,然后再添加新的依赖-->
<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.3.0</version>
</dependency>
当然这里版本是对应的,所以不需要修改版本
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
2、编写配置文件这里改为properties文件
server.port=8003
#配置数据源
spring.datasource.url=jdbc:mysql://localhost:3306/seata_storage?serverTimezone=Asia/Shanghai
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=密码
#
spring.application.name=seata-storage
#注册中心地址
spring.cloud.nacos.discovery.server-addr=localhost:8848
#seata服务组的名字
spring.cloud.alibaba.seata.tx-service-group=guangzhou
seata.service.vgroup-mapping.guangzhou=default
#扫描mapper文件
mybatis.mapper-locations=classpath:mapper/*.xml
#日志扫描
logging.level.com.ykq.dao=debug
#seata注册类型
seata.registry.type=nacos
#注册中心的信息-----------------------------
#指定seata服务器所在的注册中心地址
seata.registry.nacos.server-addr=localhost:8848
#nacos的用户名和密码
seata.registry.nacos.username=nacos
seata.registry.nacos.password=nacos
#指定seata服务器在注册中心的组名,默认为SEATA_GROUP
seata.registry.nacos.group=SEATA_GROUP
#指定seata服务器在注册中心的服务名,默认为seata-server
seata.registry.nacos.application=seata-server
#注册中心的信息----------------------------------
#指定seata服务器所在的配置中心地址
seata.config.nacos.server-addr=localhost:8848
#nacos的用户名和密码
seata.config.nacos.username=nacos
seata.config.nacos.password=nacos
#指定seata服务器在配置中心的组名,默认为SEATA_GROUP
seata.config.nacos.group=SEATA_GROUP
3、运行项目进行测试
报错: no available service 'default' found, please make sure registry config correct
在配置文件中添加一行,这里的guangzhou就是自己命名的seata组名
seata.service.vgroup-mapping.guangzhou=default
再次运行就可以成功了,就代表着客户端也搭建完毕,将其他模块修改一下即可
4、修改发起者的代码 [订单微服务的service实现类]
package com.ykq.service.impl;
import com.ykq.dao.OrderDao;
import com.ykq.entity.Order;
import com.ykq.feign.AccountFeign;
import com.ykq.feign.StorageFeign;
import com.ykq.service.OrderService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private AccountFeign accountFeign;//表示调用的远程服务。
@Autowired
private StorageFeign storageFeign;
@Override
@GlobalTransactional //使用这个注解进行分布式事务
public void saveOrder(Order order) {
log.info("-------->开始创建新订单");
orderDao.saveOrder(order);
log.info("-------订单微服务开始调用账户,做扣减");
accountFeign.increase(order.getUserId(),order.getMoney()); //当前的事务回滚只会回滚自己的数据
log.info("-------订单微服务开始调用账户,做扣减end");
//添加异常
// int c=10/0;
log.info("--------订单微服务开始调用库存,做扣减");
storageFeign.increase(order.getProductId(),order.getCount());
log.info("-------订单微服务开始调用库存,做扣减end");
log.info("-------修改订单状态");
orderDao.updateStatus(order.getId());
log.info("-------修改订单状态结束");
log.info("--------下订单结束了,哈哈哈哈");
}
}
然后发起请求[数据库已经改为最初状态 拥有1000元,库存为100,订单数为0]
1、先验证正常情况
请求:http://localhost:8001/order/addOrder/?userId=1&productId=1&count=5&money=100
请求结果:{"code":2000,"message":"下单成功","data":null}
2、再测试异常情况
请求:http://localhost:8001/order/addOrder/?userId=1&productId=1&count=5&money=100
请求结果:
{"code":5000,"message":"下单失败","data":null}
其中一个微服务的事务失败,所有的事务都会回滚,
事务过程中,生成的信息存在日志表中,事务的信息存在seata数据库的三张表中,通过打断点可以看到中间的信息