一、項目結構:
1、 父工程
pom.xml:(父工程只有一個pom.xml)
<?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.lucifer</groupId>
<artifactId>rocketmq-transaction</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>order-service</module>
<module>storage-service</module>
<module>base-framework-mysql-support</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.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>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
2、base-framework-mysql-support:數據庫相關的配置
2.1 MybatisPlusConfig :mybatis-plus的相關配置
package com.lucifer.config;
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author lucifer
* @date 2020/4/14 21:54
* @description mybatis-plus 配置
*/
@Configuration
@MapperScan(value = {"com.lucifer.mapper"})
public class MybatisPlusConfig {
/**
* SQL執行效率插件
*/
@Bean
// @Profile({"dev", "test"})// 設置 dev test 環境開啓
public PerformanceInterceptor performanceInterceptor() {
return new PerformanceInterceptor();
}
}
2.2 RedissonConfig: redisson相關配置
package com.lucifer.config;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SentinelServersConfig;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Configuration
public class RedissonConfig {
@Resource
private RedissonProperties redissonProperties;
/**
* 單機模式自動裝配
* @return
*/
@Bean
@ConditionalOnProperty(prefix ="redisson",name="single-is",havingValue="true")
public RedissonClient getSingleRedisson(){
Config config = new Config();
String singlePassword = redissonProperties.getSinglePassword();
SingleServerConfig serverConfig = config.useSingleServer().setAddress("redis://" + redissonProperties.getSingleAddress());
System.out.println("redis:=================="+serverConfig.getAddress());
if(StringUtils.isNotBlank(singlePassword)){
serverConfig.setPassword(singlePassword);
}
return Redisson.create(config);
}
/**
* 哨兵模式自動裝配
* @return
*/
@Bean
@ConditionalOnProperty(prefix ="redisson",name="sentinel-is",havingValue="true")
public RedissonClient getSentinelRedisson(){
Config config = new Config();
SentinelServersConfig serverConfig = config.useSentinelServers().addSentinelAddress(redissonProperties.getSentinelAddresses()).setMasterName(redissonProperties.getSentinelMasterName());
String sentinelPassword = redissonProperties.getSentinelPassword();
if(StringUtils.isNotBlank(sentinelPassword)) {
serverConfig.setPassword(sentinelPassword);
}
return Redisson.create(config);
}
}
2.3 RedissonProperties:讀取application.yml的自定義配置
package com.lucifer.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author lucifer
* @date 2020/4/21 14:09
* @description TODO
*/
@Data
@Component
@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties {
private String singleAddress;
private String singlePassword;
private String sentinelMasterName;
private String sentinelAddresses;
private String sentinelPassword;
}
2.4 pom.xml: jar包
<?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>rocketmq-transaction</artifactId>
<groupId>com.lucifer</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>base-framework-mysql-support</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>
3、order-service 訂單微服務
3.1 OrderController:控制層 用於測試
package com.lucifer.controller;
import com.lucifer.pojo.Order;
import com.lucifer.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.UUID;
/**
* @author lucifer
* @date 2020/4/14 19:32
* @description TODO
*/
@RestController
@RequestMapping(value = "order")
public class OrderController {
@Resource
OrderService orderService;
/**
* 下單:插入訂單表、扣減庫存,模擬回滾
*
* @return
*/
@GetMapping("/placeOrder/commit")
public Boolean placeOrderCommit() {
//將uuid作爲事務id,發送到mq
String uuid = UUID.randomUUID().toString();
Order order = new Order();
order.setCommodityCode("product-1");
order.setUserId("1");
order.setCount(1);
order.setTxNum(uuid);
order.setMoney(new BigDecimal(12.5));
System.out.println("準備下單了=======》" + order);
orderService.sendOrder(order);
return true;
}
}
3.2 service 接口:
package com.lucifer.service;
import com.lucifer.pojo.Order;
public interface OrderService {
/**
* 發送訂單消息
*
* @param order
*/
void sendOrder(Order order);
/**
* 新增訂單
*
* @param order
*/
void insertOrder(Order order) throws Exception;
}
3.2.1 service 實現類:
package com.lucifer.service.impl;
import com.alibaba.fastjson.JSON;
import com.lucifer.mapper.OrderMapper;
import com.lucifer.mapper.TxLogMapper;
import com.lucifer.pojo.Order;
import com.lucifer.pojo.Storage;
import com.lucifer.pojo.TxLog;
import com.lucifer.service.OrderService;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.redisson.api.RBucket;
import org.redisson.api.RFuture;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @author lucifer
* @date 2020/4/14 19:31
* @description
*/
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private RocketMQTemplate rocketMQTemplate;
@Resource
private OrderMapper orderMapper;
@Resource
private TxLogMapper txLogMapper;
@Resource
private RedissonClient redissonClient;
@Override
public void sendOrder(Order order) {
String str = JSON.toJSONString(order);
Message<String> message = MessageBuilder.withPayload(str).build();
/**
* 發送一條事務消息
* String txProducerGroup: 生產組
* String destination:topic
* Message<?> message: 消息內容
* Object arg: 參數
*/
rocketMQTemplate.sendMessageInTransaction("producer_group_tx1", "topic_tx", message, null);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void insertOrder(Order order) throws Exception {
//創建鎖對象
RLock lock = redissonClient.getLock("placeOrder:" + order.getUserId() + order.getCommodityCode());
try {
//嘗試去獲取鎖
RFuture<Boolean> booleanRFuture = lock.tryLockAsync(3, 10, TimeUnit.SECONDS);
Boolean aBoolean = booleanRFuture.get();
//如果獲取到了鎖
if (aBoolean) {
//獲取redis中的庫存
RBucket<Storage> storageBucket = redissonClient.getBucket("placeOrder");
Storage storage = storageBucket.get();
System.out.println("剩餘庫存:=================" + storage.getCount());
if (storage.getCount() <= 0) {
throw new RuntimeException("商品:" + order.getCommodityCode() + ",庫存爲空");
}
//用事務id冪等處理
if (txLogMapper.selectById(order.getTxNum()) != null) {
return;
}
orderMapper.insert(order);
//插入事務日誌
TxLog txLog = new TxLog();
txLog.setTxNum(order.getTxNum());
System.out.println("order.getTxNum():" + order.getTxNum());
txLog.setCreateTime(new Date());
txLogMapper.insert(txLog);
}
} finally {
//釋放鎖
lock.unlock();
}
}
}
3.3. mapper接口:
package com.lucifer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.TxLog;
public interface TxLogMapper extends BaseMapper<TxLog> {
}
package com.lucifer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.Order;
public interface OrderMapper extends BaseMapper<Order> {
}
3.4. 實體類:
package com.lucifer.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 訂單表
*/
@Data
@NoArgsConstructor
@TableName("order_tbl")
public class Order {
@TableId(type = IdType.AUTO)
private Integer id;
private String userId;
private String commodityCode;
private Integer count;
private BigDecimal money;
@TableField(exist = false)
private String txNum;
}
package com.lucifer.pojo;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 庫存表
*/
@Data
@Accessors(chain = true)
@TableName("storage_tbl")
public class Storage implements Serializable {
private Long id;
private String commodityCode;
private Long count;
}
import java.util.Date;
/**
* @author lucifer
* @date 2020/4/15 13:04
* @description 事務日誌表
*/
@Data
//@Builder
@NoArgsConstructor
//@Accessors(chain = true)
@TableName("tx_log")
public class TxLog {
@TableId
private String txNum;
private Date createTime;
}
3.5、ProducerTransactionListener 是去實現 RocketMQLocalTransactionListener接口(重要)
package com.lucifer.transaction;
import com.alibaba.fastjson.JSON;
import com.lucifer.mapper.TxLogMapper;
import com.lucifer.pojo.Order;
import com.lucifer.pojo.Storage;
import com.lucifer.pojo.TxLog;
import com.lucifer.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* @author lucifer
* @date 2020/4/15 0:59
* @description TODO
*/
@Slf4j
@Component
@RocketMQTransactionListener(txProducerGroup = "producer_group_tx1")
public class ProducerTransactionListener implements RocketMQLocalTransactionListener {
@Resource
private OrderService orderService;
@Resource
private TxLogMapper txLogMapper;
@Resource
private RedissonClient redissonClient;
/**
* 事務消息發送mq成功後的回調方法
*
* @param msg
* @param arg
* @return 返回事務狀態
* RocketMQLocalTransactionState.COMMIT:提交事務,提交後broker才允許消費者使用
* RocketMQLocalTransactionState.ROLLBACK:回滾事務,回滾後消息將被刪除,並且不允許別消費
* RocketMQLocalTransactionState.UNKNOWN:中間狀態,表示MQ需要覈對,以確定狀態
*/
@Transactional(rollbackFor = Exception.class)
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
String str = new String((byte[]) msg.getPayload());
Order order = JSON.parseObject(str, Order.class);
//下單
orderService.insertOrder(order);
//扣減redis中庫存
RBucket<Storage> storageBucket = redissonClient.getBucket("placeOrder");
Storage storage = storageBucket.get();
long count = storage.getCount() - order.getCount();
storage.setCount(count);
storageBucket.set(storage);
//當返回RocketMQLocalTransactionState.COMMIT,自動向mq發送commit,mq將消息的狀態改爲可消費
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
e.printStackTrace();
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 事務狀態回查,查詢是否下單成功
*
* @param msg
* @return 返回事務狀態
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String str = new String((byte[]) msg.getPayload());
Order order = JSON.parseObject(str, Order.class);
//事務id
String txNo = order.getTxNum();
TxLog txLog = txLogMapper.selectById(txNo);
if (txLog != null) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.UNKNOWN;
}
}
}
3.6. 啓動類
package com.lucifer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @author lucifer
* @date 2020/4/14 19:28
* @description TODO
*/
@EnableDiscoveryClient
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
3.7.application.yml:
server:
port: 8081
spring:
application:
name: order-service
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.160.131:3306/order?autoReconnect=true&useUnicode=true&createDatabaseIfNotExist=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
cloud:
nacos:
discovery:
server-addr: 192.168.160.131:8848
# main:
# allow-bean-definition-overriding: true
logging:
level:
com.lucifer.mapper: debug
rocketmq:
producer:
group: producter_tx
name-server: 192.168.160.131:9876
redisson:
single-is: true
single-address: 192.168.160.131:6379
single-password:
sentinel-is: false
sentinel-master-name: business-master
sentinel-addresses: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
sentinel-password:
3.8. pom.xml
<?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>
<parent>
<artifactId>rocketmq-transaction</artifactId>
<groupId>com.lucifer</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>order-service</artifactId>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.lucifer</groupId>
<artifactId>base-framework-mysql-support</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!-- nacos -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.storage-service 庫存微服務
4.1 項目啓動就會將商品 "product-1"的庫存信息查詢出來,放到redis當中,這裏用於測試
package com.lucifer.config;
import com.lucifer.pojo.Storage;
import com.lucifer.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Slf4j
@Component
public class StorageInitApplicationRunner implements ApplicationRunner {
@Resource
private RedissonClient redissonClient;
@Resource
private StorageService storageService;
@Override
public void run(ApplicationArguments applicationArguments) throws Exception {
//從數據庫查詢搶購秒殺商品信息
Storage storage = storageService.getStorage("product-1");
//獲取redis中key爲storage對象信息
RBucket<Storage> storageBucket = redissonClient.getBucket("placeOrder");
//如果key存在,就設置key的值爲新值value
//如果key不存在,就設置key的值爲value
storageBucket.set(storage);
log.info("商品數據初始化完成!");
}
}
4.2 service接口
package com.lucifer.service;
import com.lucifer.pojo.Storage;
import java.util.concurrent.ExecutionException;
public interface StorageService {
/**
* 扣減庫存
*
* @param commodityCode
* @param count
* @param txNum 事務id
*/
void deduct(String commodityCode, int count, String txNum) throws ExecutionException, InterruptedException, Exception;
/**
* 獲取商品信息
*
* @param commodityCode
* @return
*/
Storage getStorage(String commodityCode);
}
4.2.1 實現類
package com.lucifer.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lucifer.mapper.StorageMapper;
import com.lucifer.mapper.TxLogMapper;
import com.lucifer.pojo.Storage;
import com.lucifer.pojo.TxLog;
import com.lucifer.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* @author lucifer
* @date 2020/4/14 20:07
* @description TODO
*/
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageMapper storageMapper;
@Resource
private TxLogMapper txLogMapper;
@Resource
private RedissonClient redissonClient;
@Transactional(rollbackFor = Exception.class)
@Override
public void deduct(String commodityCode, int count, String txNum) throws Exception {
RLock lock = redissonClient.getLock("placeOrder:" + commodityCode);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
try {
if (res.get()) {
log.info("扣減庫存,商品編碼:{},數量:{}", commodityCode, count);
TxLog txLog = txLogMapper.selectById(txNum);
if (txLog != null) {
return;
}
//扣減庫存
QueryWrapper<Storage> wrapper = new QueryWrapper<>();
wrapper.setEntity(new Storage().setCommodityCode(commodityCode));
Storage storage = storageMapper.selectOne(wrapper);
if (storage == null) {
throw new RuntimeException("商品" + commodityCode + ",不存在");
}
//扣減MySQL中的庫存
storage.setCount(storage.getCount() - count);
System.out.println("剩餘庫存數量:" + storage.getCount());
storageMapper.updateById(storage);
//添加事務記錄,用於冪等
TxLog tLog = new TxLog();
tLog.setTxNum(txNum);
tLog.setCreateTime(new Date());
txLogMapper.insert(tLog);
}
} finally {
lock.unlock();
}
}
@Override
public Storage getStorage(String commodityCode) {
QueryWrapper<Storage> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("commodity_code", commodityCode);
Storage storage = storageMapper.selectOne(queryWrapper);
System.out.println("剩餘庫存數:" + storage.getCount());
return storage;
}
}
4.3 mapper接口
package com.lucifer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.Storage;
public interface StorageMapper extends BaseMapper<Storage> {
}
package com.lucifer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.TxLog;
public interface TxLogMapper extends BaseMapper<TxLog> {
}
4.4 實體類
Order、Storage、TxLog 三個實體類同order-service中;
4.5 ConsumerTransactionListener 實現RocketMQListener接口,消費消息
package com.lucifer.transaction;
import com.alibaba.fastjson.JSON;
import com.lucifer.pojo.Order;
import com.lucifer.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author lucifer
* @date 2020/4/15 0:59
* @description TODO
*/
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "consumer_group_tx2", topic = "topic_tx")
class ConsumerTransactionListener implements RocketMQListener<String> {
@Resource
private StorageService storageService;
@Override
public void onMessage(String message) {
log.info("開始消費消息:{}", message);
//解析消息
Order order = JSON.parseObject(message, Order.class);
if(order!=null){
//扣減庫存
try {
storageService.deduct(order.getCommodityCode(), order.getCount(), order.getTxNum());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("扣減庫存失敗");
}
}
}
}
4.6啓動類
package com.lucifer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @author lucifer
* @date 2020/4/14 20:23
* @description 庫存服務
*/
@EnableConfigurationProperties
@EnableDiscoveryClient
@SpringBootApplication
public class StorageApplication {
public static void main(String[] args) {
SpringApplication.run(StorageApplication.class, args);
}
}
4.7application.yml:
server:
port: 8082
spring:
application:
name: storage-service
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.160.131:3306/storage?autoReconnect=true&useUnicode=true&createDatabaseIfNotExist=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
cloud:
nacos:
discovery:
server-addr: 192.168.160.131:8848
# main:
# allow-bean-definition-overriding: true
logging:
level:
com.lucifer.mapper: debug
rocketmq:
producer:
group: consumer_tx
name-server: 192.168.160.131:9876
redisson:
single-is: true
single-address: 192.168.160.131:6379
single-password:
sentinel-is: false
sentinel-master-name: business-master
sentinel-addresses: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
sentinel-password:
4.8 pom.xml
<?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>
<parent>
<artifactId>rocketmq-transaction</artifactId>
<groupId>com.lucifer</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>storage-service</artifactId>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.lucifer</groupId>
<artifactId>base-framework-mysql-support</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<!-- nacos -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.2.2.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>fastjson</artifactId>
<groupId>com.alibaba</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
二、測試
(1)場景一:正常情況:使用jmter測試:用20個線程去模擬20個用戶同時去購買某個商品:
目前庫存只有10個:
查看控制檯: (當10個訂單下單成功後,第11個訂單。。。。)
準備下單了=======》Order(id=null, userId=1, commodityCode=product-1, count=1, money=12.5, txNum=7a0702f8-8814-4d6a-a492-04ccd1bda31b)
剩餘庫存:=================0
java.lang.RuntimeException: 商品:product-1,庫存爲空
查看數據庫:只有10個訂單下單成功,並且庫存爲0,並沒有成爲負數
(2)場景2:模擬異常的發生,在order-service中:
刪除redis數據、清空數據庫剛產生的數據,將庫存數調回10,重啓庫存微服務;
模擬20個用戶去訪問,order-service 微服務控制檯:
剩餘庫存:=================6
2020-04-21 19:20:59.502 DEBUG 22056 --- [nio-8081-exec-1] c.lucifer.mapper.TxLogMapper.selectById : ==> Preparing: SELECT tx_num,create_time FROM tx_log WHERE tx_num=?
2020-04-21 19:20:59.509 DEBUG 22056 --- [nio-8081-exec-1] c.lucifer.mapper.TxLogMapper.selectById : ==> Parameters: 4dadbd02-735f-43f3-b10e-5863c833406c(String)
2020-04-21 19:20:59.520 DEBUG 22056 --- [nio-8081-exec-1] c.lucifer.mapper.TxLogMapper.selectById : <== Total: 0
Time:17 ms - ID:com.lucifer.mapper.TxLogMapper.selectById
Execute SQL:SELECT tx_num,create_time FROM tx_log WHERE tx_num='4dadbd02-735f-43f3-b10e-5863c833406c'
java.lang.RuntimeException: 人爲異常
當訂單下了4單,剩餘庫存爲6時,人爲異常拋出,此時數據庫:
(2)場景3:模擬異常的發生,在storage-service中:
庫存微服務 控制檯:一直打印,去扣減MySQL的庫存,不過因爲此處人爲製造異常,庫存只要爲5,就會去拋異常
java.lang.RuntimeException: 扣減庫存失敗
at com.lucifer.transaction.ConsumerTransactionListener.onMessage(ConsumerTransactionListener.java:37) ~[classes/:na]
at com.lucifer.transaction.ConsumerTransactionListener.onMessage(ConsumerTransactionListener.java:18) ~[classes/:na]
at org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer$DefaultMessageListenerConcurrently.consumeMessage(DefaultRocketMQListenerContainer.java:308) ~[rocketmq-spring-boot-2.0.2.jar:2.0.2]
at org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService$ConsumeRequest.run(ConsumeMessageConcurrentlyService.java:417) [rocketmq-client-4.4.0.jar:4.4.0]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_192]
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266) [na:1.8.0_192]
at java.util.concurrent.FutureTask.run(FutureTask.java) [na:1.8.0_192]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_192]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_192]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_192]
2020-04-21 19:31:31.243 DEBUG 15160 --- [essageThread_12] c.lucifer.mapper.TxLogMapper.selectById : ==> Parameters: f60a6c44-292b-47b7-8bfd-6aedaeb26a8e(String)
java.lang.RuntimeException: 人爲異常
此時,order數據庫中有10個訂單,而庫存數據庫中庫存爲6,有6單是扣減失敗的,所以此時庫存微服務會不停的去嘗試扣減庫存(嘗試次數好像是16次),一般情況下,不會讓rocketmq重試那麼多次,重試幾次差不多了,還是無法扣減只能人工扣減了。
此時將IDEA中的人爲異常註釋掉,重新編譯,會發現,庫存微服務扣減成功了,然後數據庫中訂單數爲10,庫存數扣減爲0 了。