天天看点

微服务分布式事务问题示例

前言

本次示例的场景是下订单减库存。

用到了springboot+springcloud+mybatis+mysql。

数据库有订单表和库存表

微服务分布式事务问题示例
微服务分布式事务问题示例

springboot整合mybatis和mysql的详细讲解可以看此博客

https://blog.csdn.net/daziyuanazhen/article/details/100822121

创建eureka注册中心项目

添加依赖

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>lcneureka</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>lcneureka</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <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>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
           

修改application.properties配置文件

spring.application.name=eureka
server.port=8080

eureka.client.register-with-eureka=false  
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://localhost:8080/eureka
           

修改springboot入口类

@SpringBootApplication
@EnableEurekaServer
public class LcneurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(LcneurekaApplication.class, args);
    }

}
           

创建订单项目

添加依赖

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>lcnservicea</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>lcnservicea</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <!-- mybatis依赖 begin -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!-- mybatis依赖 end -->
        <!-- mysql数据库配置 begin -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- mysql数据库配置 end -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <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>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
           

修改application.properties配置文件

server.port=8081
spring.application.name=service-order 
eureka.client.service-url.defaultZone=http://localhost:8080/eureka


##数据库配置
##数据库地址
spring.datasource.url=jdbc:mysql://localhost:3306/blog?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
##数据库用户名
spring.datasource.username=root
##数据库密码
spring.datasource.password=123456
##数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver


mybatis.config-location=classpath:mybatis/mybatis-config.xml
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
           

在resources文件夹下创建配置文件

微服务分布式事务问题示例

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <typeAlias alias="Integer" type="java.lang.Integer" />
        <typeAlias alias="Long" type="java.lang.Long" />
        <typeAlias alias="HashMap" type="java.util.HashMap" />
        <typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap" />
        <typeAlias alias="ArrayList" type="java.util.ArrayList" />
        <typeAlias alias="LinkedList" type="java.util.LinkedList" />
    </typeAliases>
</configuration>
           

OrderMapper.xml

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">


<mapper namespace="com.example.lcnservicea.OrderMapper">
    <insert id="addOrder">
        INSERT INTO o_order VALUE('asd','2019-09-13 21:14:00')
    </insert>
</mapper>
           

创建业务文件

微服务分布式事务问题示例

新建库存服务的feign调用文件 ServiceBFeign.java

package com.example.lcnservicea;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@FeignClient(value="service-stock")
public interface ServiceBFeign {

    @RequestMapping(value = "/reduceStock",method = RequestMethod.GET)
    void reduceStock();
}
           

新建OrderMapper.java

package com.example.lcnservicea;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper {

    void addOrder();
}
           

新建OrderService.java

package com.example.lcnservicea;

public interface OrderService {

    void addOrder();
}
           

新建OrderServiceImpl.java

package com.example.lcnservicea;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = false,rollbackFor = Exception.class)
public class OrderServiceImpl implements OrderService {
    @Autowired
    private ServiceBFeign serviceBFeign;
    @Autowired
    private OrderMapper orderMapper;

    @Override
    public void addOrder() {
        orderMapper.addOrder();
        System.out.println("添加订单成功");
        serviceBFeign.reduceStock();
        System.out.println("调用库存服务减库存成功");

        System.out.println("出现异常,事务回滚");
        int i = 1/0;

    }
}
           

新建OrderController.java

package com.example.lcnservicea;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @RequestMapping("/addOrder")
    public String addOrder(){
        orderService.addOrder();
        return "ok";
    }
}
           

修改springboot入口类

package com.example.lcnservicea;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableFeignClients
public class LcnserviceaApplication {

    public static void main(String[] args) {
        SpringApplication.run(LcnserviceaApplication.class, args);
    }

}
           

创建库存项目

微服务分布式事务问题示例

跟订单项目相似,不同的是没有引入feign,业务文件也不一样

StockMapper.xml

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">


<mapper namespace="com.example.lcnserviceb.StockMapper">
    <update id="reduceStock">
        UPDATE s_stock SET stockNum = stockNum - 1
    </update>
</mapper>
           

StockServiceImpl.java

package com.example.lcnserviceb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = false,rollbackFor = Exception.class)
public class StockServiceImpl implements StockService {
    @Autowired
    private StockMapper stockMapper;

    @Override
    public void reduceStock() {
        stockMapper.reduceStock();
        System.out.println("减库存成功。。。。");
    }
}
           

StockController.java

package com.example.lcnserviceb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StockController {
    @Autowired
    private  StockService stockService;

    @RequestMapping("/reduceStock")
    public void reduceStock(){
        stockService.reduceStock();
    }
}
           

springboot入口类

package com.example.lcnserviceb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;


@SpringBootApplication
@EnableEurekaClient
public class LcnservicebApplication {

    public static void main(String[] args) {
        SpringApplication.run(LcnservicebApplication.class, args);
    }

}
           

到此,项目的准备就完成了,接下来

先启动注册中心服务,然后启动订单服务和库存服务。

打开浏览器访问http://localhost:8081/order/addOrder

可以发现订单项目报了/ by zero的异常,库存项目无异常。

打开数据库发现订单数据库并没有新增一条记录

微服务分布式事务问题示例

库存数据库库存减了1,变成99

微服务分布式事务问题示例

说明订单服务调用了库存服务后,库存服务正常运行,减了库存1,之后订单服务由于出现了/ by zero异常导致事务回滚,没有添加订单成功,这就出现了分布式事务问题,导致了少卖问题(即没有添加订单,而库存少了)。

继续阅读