听说公司上批员工入职培训有这么一个小demo
之前分布式这块接触比较少,于是尝试做了一下
这边技术选型springcloud,kafka,mysql,docker,quartz
首先描述下我这边的场景,也是很常见的一个异步调用场景:
即将服务A假设为某电商用户模块,服务B假设为电商活动模块。
我这边呢,假设用户支付多少钱,就返多少钱的一个代金券
一致性解决
梳理一下流程,上面这一版有一个致命的问题!如下所示
事务开始
(1)给alili账户扣10元
(2)给alili账户发10元代金券封装为消息,发送给消息队列
事务结束
那么来了,如何保证第一步和第二步是在同一个事务里完成的。换句话说,第一步操作的是数据库,第二步操作的是一个消息队列,你如何保证这两步之间的一致性?
记住了,任何涉及到数据库和中间件之间的业务逻辑操作,都需要考虑二者之间的一致性。比如,你先操作了数据库,再操作缓存,数据库和缓存之间一致性如何解决?
改变思路,加一张事务表,如下图所示
注意了,此时事务的内容为
事务开始
(1)给账户alili,扣10元
(2)给事件表插入一条记录
事务结束
此时是对同一数据库的两张表操作,因此可以用数据库的事务进行保证。
另外,起一个定时程序,定时扫描事务表,发现一个状态为'UNFINISHED'的事件,就进行封装为消息,发送到消息中间件,然后将状态改为'FINISHED'.
幂等性解决
注意了,这一版还存在一个幂等性问题!
仔细看,定时程序做了如下三个操作
(1)定时扫描事务表,发现一个状态为'UNFINISHED'的事件
(2)将事件信息,封装为消息,发送到消息中间件
(3)将事件状态改为'FINISHED'
假设在步骤(2)的时候,发送完消息体,还未执行步骤(3),定时程序阵亡了!然后重启定时程序,发现刚那个事务的状态依然为'UNFINISHED',因此重新发送。这样,就会出现重复消费问题。因此,幂等性也是需要保证的!
给代金券表添加事务id字段,如果一旦出现重复消费,则在事务里直接报出唯一约束冲突错误,从而保证了幂等性!
是不是觉得到这里就完了?hiahia,
消费者确定消费
仔细想,我这边消息队列是给消费者消息了,然后是不是就这个消息会在队列中清除了,
是的,这就是消息队列默认的自动ack机制,ack简单说就是个确认信息,
也就是说我消费者拿到这条信息就告诉队列我已经拿到了,你可以删除了,那这边又出现了一个问题,消费者怎么能确定自己手上这条信息在流程中不会出问题呢,按道理我们是要消费者做完事情在告诉队列去删除,我出问题了你下次再给我重发我再次消费,所以这里我们要开启手动ack在执行完业务逻辑后手动提交,以此来保证整个流程的数据一致性。
docker环境安装略。。
安装dokcer 镜像
docker pull nacos/nacos-server
启动nacos
docker run -d --env MODE=standalone -p 8848:8848 --name nacos nacos/nacos-server
访问页面
http://192.168.10.11:8848/nacos
略
我这边采用mysql5.7
讲一下nacos持久化
进入nacos容器,进入conf,拷贝sql文件,建库建表
同目录
vim application.properties
修改数据库配置,退出容器exit,docker restart
由于kafka需依赖zookeeper,我们需要容器内部通信
我这边使用docker-compose,省去手动配置,
安装docker-compose,略
新建docker-conpose.yml文件
写入配置
version: '2'
services:
zookeeper:
image: zookeeper
container_name: zookeeper
ports:
- "2181:2181"
networks:
- kafka-network
kafka:
image: wurstmeister/kafka
container_name: kafka
depends_on:
- zookeeper
links:
- zookeeper
networks:
- kafka-network
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 5
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://192.168.10.11:9092 #宿主机监听端口
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
kafka-network:
driver: bridge
在docker-compose.yml所在目录下执行:
启动
docker-compose up -d
关闭
docker-compose down
这边都是单机配置,集群自行研究,重点不在这里
用户表
CREATE TABLE `t_user` (
`uid` bigint(20) NOT NULL AUTO_INCREMENT,
`uname` varchar(100) NOT NULL COMMENT '用户名',
`money` bigint(100) NOT NULL DEFAULT '0' COMMENT '余额',
PRIMARY KEY (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
代金券事务表
CREATE TABLE `t_user_coupon_tran` (
`tid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '事务id',
`uid` bigint(20) NOT NULL COMMENT '用户id',
`coupon_money` bigint(20) NOT NULL COMMENT '优惠金额',
`status` int(1) NOT NULL COMMENT '状态',
PRIMARY KEY (`tid`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
模块结构
application.yml
server:
port: 8001
spring:
application:
name: user-server
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.10.11:3306/tran?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false
username: root
password: root
cloud:
nacos:
discovery:
server-addr: 192.168.10.11:8848
kafka:
bootstrap-servers: 192.168.10.11:9092
producer:
retries: 1
batch-size: 16384
buffer-memory: 33554432
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
properties:
linger.ms: 1
acks: all
quartz:
#持久化到数据库方式
job-store-type: jdbc
initialize-schema: embedded
properties:
org:
quartz:
scheduler:
instanceName: MyScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: true
clusterCheckinInterval: 10000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
global-config:
db-config:
id-type: auto
用户支付
@Override
@Transient
public void post(Long uid, Long money) {
User user = userDao.selectById(uid);
if(Objects.isNull(user))
throw new RuntimeException("用户不存在");
if(user.getMoney()
代金券事务状态枚举类
public enum ToCouponStatus {
USER_COUPON_NO_SEND(0,"代金券消息未发送"),USER_COUPON_SEND(1,"代金券消息已发送队列");
private Integer code;
private String status;
ToCouponStatus(Integer code, String status) {
this.code = code;
this.status = status;
}
public Integer getCode() {
return code;
}
public String getStatus() {
return status;
}
}
代金券状态监控定时任务job
public class CouponJob extends QuartzJobBean {
@Resource
private UserCouponTranService userCouponTranService;
@Resource
private KafkaTemplate kafkaTemplate;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
List tranMessages = userCouponTranService.list(new QueryWrapper()
.eq("status", ToCouponStatus.USER_COUPON_NO_SEND.getCode()));
tranMessages.forEach(tranMsg->{
//通知发放代金券
kafkaTemplate.send("coupon",tranMsg.getUid()+","+tranMsg.getCouponMoney()+","+tranMsg.getTid())
.addCallback(new ListenableFutureCallback>() {
@Override
public void onFailure(Throwable throwable) {
throw new RuntimeException("消息发送失败");
}
@Override
public void onSuccess(SendResult stringObjectSendResult) {
//更改事务记录状态
tranMsg.setStatus(ToCouponStatus.USER_COUPON_SEND.getCode());
userCouponTranService.updateById(tranMsg);
}
});
});
}
}
代金券表
CREATE TABLE `t_coupon` (
`cid` bigint(20) NOT NULL AUTO_INCREMENT,
`cmoney` bigint(20) NOT NULL COMMENT '优惠金额',
`uid` bigint(20) NOT NULL COMMENT '用户id',
`tid` bigint(20) NOT NULL COMMENT '事务id',
PRIMARY KEY (`cid`),
UNIQUE KEY `t_coupon_tid_unique_key` (`tid`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
application.yml
server:
port: 8002
spring:
application:
name: activity-server
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.10.11:3306/tran?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false
username: root
password: root
cloud:
nacos:
discovery:
server-addr: 192.168.10.11:8848
kafka:
bootstrap-servers: 192.168.10.11:9092
consumer:
group-id: test
auto-offset-reset: earliest
enable-auto-commit: false
auto-commit-interval: 1000
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
global-config:
db-config:
id-type: auto
手动监听容器工厂配置
@Configuration
public class kafkaConf {
/**
* MANUAL_IMMEDIATE 手动调用Acknowledgment.acknowledge()后立即提交
* @param consumerFactory
* @return
*/
@Bean("manualImmediateListenerContainerFactory")
public KafkaListenerContainerFactory> manualImmediateListenerContainerFactory(
ConsumerFactory consumerFactory) {
ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
factory.getContainerProperties().setPollTimeout(1500);
factory.setBatchListener(true);
//配置手动提交offset
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
}
消费端监听
@Component
@Log
public class CouponListener {
@Resource
private CouponService couponService;
@KafkaListener(containerFactory = "manualImmediateListenerContainerFactory", topics = "coupon")
public void grantCoupon(List
https://gitee.com/alili0619/tran
欢迎交流!
https://www.cnblogs.com/rjzheng/p/10115798.html
https://www.cnblogs.com/rjzheng/p/8994962.html
https://blog.csdn.net/qq330983778/article/details/105937689