商城—微服务技术栈
1 商城介绍
1.1 项目介绍
此电商项目属于B2C模式的线上商城,支持用户在线浏览商品,在线搜索商品,并且可以将喜欢的商品加入购物车从而下单购买商品,同时支持线上支付,支付模式支持支付宝、微信、银联支付。用户还可以参与低价商品秒杀。
畅购商城采用了微服务架构,微服务技术采用了SpringCloud技术栈,各个微服务站点基于SpringBoot构建,并采用SpringCloud Gateway将各个微服务的功能串联起来,形成一套套系统,同时在微服务网关Gateway中采用过滤和限流策略,实施对微服务进行保护和权限认证操作。项目采用了SpringSecurity OAuth2.0解决了各个微服务之间的单点登录和用户授权。采用了当前非常热门的Seata来解决微服务与微服务之间的分布式事务。采用了Elasticsearch解决了海量商品的实时检索。数据存储采用了MySQL,并结合Canal实现数据同步操作,利用Redis做数据缓存操作。各个微服务之间采用RabbitMQ实现异步通信。我们采用了OpenResty集成的Nginx来控制微服务最外层的大量并发,利用Keepalived+Nginx来解决Nginx单点故障问题。
2 商城业务总结
2.1 购物车业务
购物车分为2种情况,一种是用户登录或者不登录均能使用购物车,另一种是用户只能登录后使用购物车。
方案一:登录/不登录均能使用购物车
如果用户不登录也能使用购物车,此时我们会将购物车信息存入到Cookie中,如果用户浏览器禁用了Cookie,我们可以将购物车数据存入到localStorage或者WebSQL中。
用户登录后,购物车数据存入到Redis中。
如果用户从未登录切换到登录状态,此时需要将用户Cookie/WebSQL/localStorage中的数据合并到Redis中。
该购物车实现方式比较方便,用户不登录也能使用购物车,但也存在安全问题,例如:未登录将商品加入购物车,清空浏览器缓存后数据就没了。
方案二:用户必须登录才能使用购物车
用户如果不登录就无法使用购物车,该方案会将用户数据存入到Redis中,使用该方案可以确保用户数据安全。
用户将商品加入购物车后,我们会将购物车数据存入到Redis中,数据类型选中Hash类型,将用户的名字作为namespace,把要加入购物车的商品ID作为key,加入购物车的商品信息作为value。
订单流程:
用户添加订单的时候,可以在购物车列表页选择将哪些商品添加到订单中并下单操作。点击结算的时候,会进入到下单页面,下单页面会显示用户选中的购物车商品信息,点击提交订单的时候,会实现订单的添加操作。
这里需要注意下,添加订单后会将订单中的商品信息从购物车中移除,创建订单之前需要校验一下数据库中商品的价格,此时价格以数据库中的商品价格为准,下单后还需要调用商品微服务实现库存的递减操作,库存递减这里需要控制库存超卖问题,解决超卖问题可以使用数据库的行级锁(for update 悲观锁)实现。
用如下SQL语句操作,基于num>#{num}实现乐观锁:UPDATE tb_sku SET num=num-#{num} WHERE id=#{id} AND num>=#{num}
当返回所减库存>=剩余库存的时候,才表示减库存操作成功,否则抛出异常实现数据回滚。
乐观锁效率高于悲观锁,因为没有线程阻塞,最多也就是减库存失败,实现回滚。
支付流程:
1)用户在订单微服务中下单,下单后,会向MQ发送一个延时队列,延时30分钟,队列的信息主要是订单信息。
2)下单成功后,前端调用支付系统,根据订单号创建支付二维码,支付二维码的支付地址由订单微服务调用微信服务获取支付地址。
3)用户看到支付二维码后,开始扫码付款,并进行授权操作,此时如果授权成功,则用户扣款支付成功。
4)用户支付成功后,微信服务器会将支付状态发送给支付微服务,支付微服务会将该信息发给RabbitMQ。
5)在订单系统中会创建一个支付信息消息的监听,读到支付信息后,根据支付结果,如果支付成功,则修改订单状态,如果是支付失败,也修改对应订单状态。
6)防止用户长时间不支付,在订单系统中,还编写一个延时队列监听,半小时后,能监听到订单信息,并在数据库中检查订单是否已经支付,如果已经支付,则无需处理,如果未支付,则需要关闭微信支付,同时修改订单状态和商品状态。
问题说明:
支付中如果出现网络故障,而没有收到微信服务的支付装填响应没怎么处理?
微信服务器提供了一个根据订单号查询支付信息的方法,可以调用该方法查询支付状态信息。
微信支付一个订单付款两次问题处理
一是,网络延迟造成回调的数据包返回超时,没有收到支付成功状态,
二是,还有可能用户在支付的时候订单状态没有及时更新有问题。
三是,开始苦逼的支付测试,忽然发现有一单的订单支付确实成功后收不到任何状态信息,这就奇怪了,开始查问题,是不是服务器拦截了微信支付的回调,忽然想到上线的后,因为受到很多恶意ip攻击,把阿里云 云盾中记录的恶意ip屏蔽 ,大多恶意ip 都是出自一个网段,通过IP段方式加入阿里云安全组规则 屏蔽了。难道是微信支付服务器IP地址恰好增加了又在这个屏蔽的ip段内,赶紧调微信支付接口查看ip看下果然,增加很多微信支付的IP地址和ip段,大概是150多个ip了,这个坑终于是找到原因了,希望遇到这种问题朋友也各有好运咯。
秒杀中的常见问题的解决
1)解决超卖的问题
1)Redis预减库存,有一个下单请求过来时预减库存,若减完后的redis库存小于0说明已经卖完,此时直接返回客户端已经卖完。后续使用内存标记,减少Redis访问。若预减库存成功,则异步下单,请求入队列,返回客户端排队中。
2)数据库层面防止超卖:Redis预减库存只是抢到了这个机会,真正是否购买成功还是要等到所有数据库操作的真正成功,即消息队列的消费端是否消费成功。
数据库层面,秒杀的订单表设置唯一索引,防止重复下单。
数据库层面,减库存的时候同时判断此时库存是否大于0。
由于是订单模块和库存模块,涉及到分布式事务的问题,使用seata框架。
2)如何保证Redis的库存与数据库库存的一致性
在我们的项目中,Redis的库存并不是真正的库存,而是用于阻挡多余的下单请求,用于保证有多少秒杀商品库存就放多少个请求到消息队列,大大减少数据库访问。
真正的下单和减库存操作还是操作数据库的。
所以我们不需要保证Redis缓存与数据库的一致性。
3)如何保证MQ不丢失消息
消息丢失的三种情况:
**生产者丢失消息:**生产者发送消息到MQ时因为网络问题丢失消息。
**MQ丢失消息:**没来得及持久化,就挂掉后消息丢失
**消费端丢失消息:**刚从MQ获取消息,没处理完消费者就挂掉了
1 生产者丢失消息的解决方案:
1) rabbitmq提供事务支持,在生产者发送之前开启事务,然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会受到异常报错,这时就可以回滚事物,然后尝试重新发送;如果收到了消息,那么就可以提交事物。但是这种方案会阻塞生产者,吞吐量下降。
2)==可以将channel开启confirm机制。==在生产者哪里设置开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后写入了rabbitmq之中;**如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。**如果成功发送到mq,也会回调一个ack的接口方法,告诉你成功发送消息。
这里使用方案二!这样吞吐量更高。
2 MQ自己丢失数据的解决方案
设置持久化!持久化有两个步骤:
1)创建queue时设置持久化,但是这时候持久化的是queue的元数据,不会持久化queue里面的数据。
2)发送消息的时候将消息的deliveryMode设置为2,表示将消息持久化。
而且可以将持久化与生产者的confirm机制结合,只有持久化成功后才回调ack方法。超时未持久化成功或持久化失败也会回调nack。
3 消费者丢失数据的解决方案:
消费者端丢失数据都是因为开启了rabbitmq的autoACK功能,即消费者获取了数据之后就自动告诉MQ已经消费。
解决方案:==关闭rabbitmq的autoAck,==在确保消息被消费成功之后才发送ACK。消息没有成功消费的话rabbitmq会重发消息,这样能保证消息不会再消费者端丢失。
4)MQ如何保证不重复消费
这里不保证不重复消费,因为保证了消息不丢失就有可能读取重复的消息。这里保证接口的幂等性即可。
在保证幂等性的基础上,因为写入MQ中的数据都有一个唯一编号,当MQ消费成功后立即往redis中写入该编号。在消费端,读取MQ数据后先判断是否已经消费过。
5)如何保证分布式事务
在项目中,MQ消费端需要保证下订单和减库存同时成功或失败,这就涉及到事务的问题。
由于是分布式架构,每一个模块对应自己的数据库,跨库之间的事务就需要分布式事务解决方案。
当然,==还有一种简单的方案,秒杀模块单独建立自己的订单表、自己的秒杀商品库存表,==即在秒杀下单业务中没有调用其他模块的接口,此时也就简单了,没有分布式事务的存在,采用本地事务解决问题。
在这里,暂且考虑分布式事务,学技术为主。尝试使用SpringCloudAlibaba提供的Seata组件去完成分布式事务,只需要加上==@GlobalTransaction注解==
通过TC、TM、RM三个组件完成:全局事务管理者、事务发起方、事务的参与方。
Seata事务的执行流程(默认是使用二阶段提交):
TM开启分布式事务(TM向TC注册全局事务记录)
按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态)
TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚事务)
TC汇总事务信息,决定事务是提交还是回滚
TC通知所有的RM提交或回滚资源,事务的二阶段结束。
在MQ消费端:
@RabbitListener(queues = MQConfig.SECKILL_QUEUE)
public void receiveSkInfo(Channel channel, MSMessage message, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
String courseId = message.getCourseId();
String id = message.getId();
String memberId = message.getMemberId();
//这里不需要判断库存,而是执行SQL的时候检查是否超卖
// 获取商品的库存
// QueryWrapper wrapper = new QueryWrapper<>();
// wrapper.eq(“course_id”, courseId);
//
// MsCourse msCourse = msCourseService.getOne(wrapper);
// Integer stockCount = msCourse.getCount();
// if (stockCount <= 0) {
// //此时已经卖完了
// channel.basicAck(tag, false); //手动确认
// return;
// }
// 判断是否已经秒杀到了(保证幂等性)
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("course_id", courseId);
wrapper.eq("member_id", memberId);
MsOrder one = msOrderService.getOne(wrapper);
if (one != null) {
channel.basicAck(tag, false); //手动确认
return;
}
// 1.减库存 2.写入订单 3.写入秒杀订单
try {
boolean flag = msOrderService.createOrders(id, courseId, memberId);
channel.basicAck(tag, false); //手动确认
}catch (Exception e){
e.printStackTrace();
channel.basicNack(tag, false, false); //
}
}
减库存、写订单的全局事务处理
@Override
@GlobalTransaction(rollbackFor=Exception.class) //分布式事务
public boolean createOrders(String orderId, String courseId, String memberId) throws Exception {
//减库存
int res = courseService.reduceCount(courseId);//这条SQL语句就会检查超卖的情况
if(res <= 0) throw new RuntimeException(“减库存失败”);
//写订单
MsOrder msOrder = new MsOrder();
msOrder.setId(orderId);
msOrder.setCourseId(courseId);
msOrder.setMemberId(memberId);
flag = orderService.save(msOrder);
if(!flag) throw new RuntimeException(“写入订单失败”);
return true;
}
– 在减库存时同时查询库存数量是否大于0,返回结果如果为0,说明更新失败,库存此时不是大于0
update ms_course set count = count -1 where course_id=#{courseId} and count>0;
1
2
请求下订单时的逻辑
//用于标记商品是否卖完。HashMap虽然不是线程安全,但是不影响,因为只会写入true。
private volatile Map
@PostMapping("createOrder/{courseId}")
@ApiOperation("下订单")
public R createOrder(@PathVariable("courseId") String courseId, HttpServletRequest request) {
// 1 判断用户是否登录
// String memberId = JwtUtils.getMemberIdByJwtToken(request);
// 随机生成用户Id,用于压测
String memberId = UUID.randomUUID().toString().substring(0,18);
if(StringUtils.isEmpty(memberId)){
return R.ok().code(20001).message(“请先登录”);
}
//2 生成订单
// 2.1基于内存判断商品是否已经秒杀完毕,减少redis访问
Boolean flag = localMap.get(courseId);
if (flag != null && flag) {
return R.error().message("已卖完");
}
// 2.2 判断是否重复秒杀(单机redis不用分布式锁,而且后面会使用redis的setnx往redis中写入订单信息,不会导致重复下单)
String orderKey = "order::"+memberId+"::"+courseId;
Boolean hasOrder = redisTemplate.hasKey(orderKey);
if(hasOrder){
return R.error().message("重复秒杀");
}
// 2.3 在redis中预减库存
//预减库存
String stock = "msCourse::" + courseId + "::count";
Long count = redisTemplate.opsForValue().decrement(stock);
if (count >= 0) {
if(count == 0) localMap.put(courseId, true);
//生成订单信息,加入MQ去持久化
// 使用redis的setnx保证不会重复下单,虽然2.2中判断了是否重复下单,但这里在多线程下任然可能重复
if(redisTemplate.opsForValue().setIfAbsent(orderKey, "", 60, TimeUnit.SECONDS)){
//生成订单。
String orderId = UUID.randomUUID().toString().replace("-", "").substring(0, 18);
MSMessage msMessage = new MSMessage(orderId, courseId, memberId);
mqProvider.sendMessage(msMessage);
return R.ok().message("校验订单,请稍后");
}else {
redisTemplate.opsForValue().increment("msCourse::" + courseId + "::count");
return R.error().message("重复秒杀");
}
}else {
//小于0说明库存为空还去减,此时要将库存加回去
redisTemplate.opsForValue().increment("msCourse::" + courseId + "::count");
return R.error().message("已卖完");
}
}
}
3 技术总结
3.1 FastDFS面试题
3.1.1 什么是FastDFS?
FastDFS是一套分布式文件管理系统,为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
1.Tracker:
负责任务调度,负载均衡管理以及Storage的注册中心功能
2.Storage(操作文件):
负责文件上传和下载以及文件的删除等管理功能
3.1.2 FastDFS有什么缺点
1.不支持文件分片,FastDFS不适合大文件存储
2.同步机制不支持文件正确性校验,降低了系统的可用性
3.2 Nginx面试题
Nginx介绍:
Nginx是一款==轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like 协议下发行。其特点是占有内存少,并发能力强,==事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。
3.2.1 什么是OpenResty?
==OpenResty 简单理解成 就相当于封装了nginx,并且集成了LUA脚本,==开发人员只需要简单提供了对应的Lua模块就可以实现相关的逻辑,而不再像之前,还需要在nginx中自己编写lua的脚本,再进行调用了。
OpenResty可以 快速构造出足以胜任 10K 以上并发连接响应的超高性能 Web 应用系统。
3.2.2 Nginx负载均衡
upstream:负载均衡池,主要实现多个节点集群,并且能实现负载均衡策略配置
负载均衡策略:
ip_hash:哈希原则
weight:权重
轮询:默认
3.2.3 Nginx限流策略
nginx提供两种限流的方式:
1.一是控制速率 控制速率的方式之一就是采用漏桶算法。
2.二是控制并发连接数
漏桶算法实现控制速率限流:
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,==漏桶以一定的速度出水(接口有响应速率),==当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:
3.2.4 Nginx动静分离
动静分离, 使用Nginx将动态请求和静态请求分离开来, 减少不必要的请求消耗, 同时能减少请求的延时。
静态数据配置:
//所有图片|静态页|css|js等 请求使用 http://source.xxx.com
//所有的请求将直接使用Nginx在 /usr/local/server/static下面去找对应的静态资源,而不需要经过Nginx
server {
listener 80;
server_name source.xxx.com;
location / {
root /usr/local/server/static;
}
}
动态请求配置:
//所有非静态资源请求 使用http://www.xxx.com
//所有请求会直接路由给 http://127.0.0.1:8080的服务器处理
server {
listener 80;
server_name www.xxx.com
location / {
proxy_pass http:127.0.0.1:8080;
}
}
3.3 Canal 面试题
canal可以用来监控数据库数据的变化,从而获得新增数据,或者修改的数据。
原理相对比较简单:
canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送ump协议
mysql master收到dump请求,开始推送binary log给slave(也就是canal)
canal解析binary log对象(原始为byte流)
3.3.2 项目中用Canal做什么事情?
canal在项目中,=主要用于做数据增量同步操作,可以将数据同步到Redis、其他MySQL、Elasticsearch等==,在我们项目中主要实现了广告缓存同步,网站公告,商品数据,击穿缓存更新,商品缓存数据更新,Elasticsearch索引库数据增量更新。
3.4 微服务网关Gateway面试题
3.4.1 为什么要使用微服务网关
项目中,用户请求由Nginx进行拦截处理,再将请求路由给微服务网关,微服务网关再将请求路由给其他微服务,通过微服务网关可以整合相关功能,所以项目中微服务网关不止一套(集群)。在微服务网关这里,还可以实现限流、鉴权相关操作。
3.4.2 微服务网关有什么优点?
优点如下:
==安全。==只有网关系统对外进行暴露,微服务可以隐藏在内网,通过防火墙保护。
==易于监控。==可以在网关收集监控数据并将其推送到外部系统进行分析。
==易于认证。==可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
减少了客户端与各个微服务之间的交互次数,易于统一授权。
3.4.3 微服务网关中如何实现限流?
在微服务网关中,我们使用令牌桶算法实现限流。
令牌桶算法是比较常见的限流算法之一,大概描述如下:
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流
3.5 JWT令牌
3.5.1 什么是JWT令牌?主要用它来做什么?
**JSON Web Token(JWT)**是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
简单来说:==JWT令牌是一段JSON字符串,主要用于封装用户身份相关信息,==可在各个微服务之间进行传递,从而识别用户的身份信息,通过它可以解决微服务中单点登录问题。
3.5.2 JWT令牌有哪些组成?
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
头部(Header)
==头部用于描述关于该JWT的最基本的信息,==例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{“typ”:“JWT”,“alg”:“HS256”}
载荷(playload)
=载荷就是存放有效信息的地方。
签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret 秘钥
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
3.5.3 如何防止令牌被盗用?
令牌:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJObzAwMDEiLCJpYXQiOjE1NjkxNTg4MDgsInN1YiI6IuS4u-mimCIsImlzcyI6Ind3dy5pdGhlaW1hLmNvbSIsImV4cCI6MTU2OTE1ODgyMywiYWRkcmVzcyI6IuS4reWbvSIsIm1vbmV5IjoxMDAsImFnZSI6MjV9.lkaOahBKcQ-c8sBPp1Op-siL2k6RiwcEiR17JsZDw98
如果令牌被盗,只要该令牌不过期,任何服务都可以使用该令牌,有可能引起不安全操作。我们可以在每次生成令牌的时候,将用户的客户端信息获取,同时获取用户的IP信息,然后将IP和客户端信息以MD5的方式进行加密,放到令牌中作为载荷的一部分,用户每次访问微服务的时候,要先经过微服务网关,此时我们也获取用户客户端信息,同时获取用户的IP,然后将IP和客户端信息拼接到一起再进行MD5加密,如果MD5值和载荷不一致,说明用户的IP发生了变化或者终端发生了变化,有被盗的嫌疑,此时不让访问即可。这种解决方案比较有效。
当然,还有一些别的方法也能减少令牌被盗用的概率,例如设置令牌超时时间不要太长。
3.6 OAuth2.0
3.6.1 什么是OAuth2.0
OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本。OAuth2.0提供了4种授权模式,分别为授权码模式授权、密码模式授权 、隐式模式授权、客户端模式授权,其中常用的是授权码模式授权和密码模式授权。
3.6.2 OAuth2.0授权码认证流程
1、客户端请求第三方授权(微信:提供授权码,发放令牌)
2、用户(资源拥有者)同意给客户端授权
3、客户端获取到授权码,请求认证服务器申请 令牌
4、认证服务器向客户端响应令牌
5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
6、资源服务器返回受保护资源
1.用户登录成功后,会将令牌信息存入到cookie中(一般建议存入到头文件中)
2.用户携带Cookie(或者Header)中的令牌访问微服务网关
3.微服务网关先获取头文件中的令牌信息,如果Header中没有Authorization令牌信息,则取参数中找,参数中如果没有,则取Cookie中找Authorization,最后将令牌信息封装到Header中,并调用其他微服务
4.其他微服务会获取头文件中的Authorization令牌信息,然后匹配令牌数据是否能使用公钥解密,如果解密成功说明用户已登录,解密失败,说明用户未登录
3.6.4 微服务与微服务之间如何实现认证?
微服务与微服务之间实现认证,只需要将用户传递的令牌Authorization传递给其他微服务即可。如果微服务之间相互调用采用的是Feign模式,可以创建一个拦截器(实现接口:RequestInterceptor ),每次执行请求之间,将令牌添加到头文件中即可传递给其他微服务。
```c
```java
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
try {
//使用RequestContextHolder工具获取request相关变量
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
//取出request
HttpServletRequest request = attributes.getRequest();
//获取所有头文件信息的key
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
//头文件的key
String name = headerNames.nextElement();
//头文件的value
String values = request.getHeader(name);
//将令牌数据添加到头文件中
requestTemplate.header(name, values);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.6.5 微服务之间如果开启了熔断限流,令牌能否传递过去?如果不能该如何解决
微服务之间相互调用,如果使用Feign调用,如果开启feign熔断,默认采用的是线程,feign调用和请求的线程不属于同一个线程,无法获取请求的线程数据,会造成空指针异常。
解决方案:hystrix隔离策略换为SEMAPHORE(信号量隔离)
隔离区别:
比较项 | 线程池隔离 | ==信号量隔离 == |
---|---|---|
线程 | 与调用线程非相同线程 | 与调用线程相同(jetty线程) |
– | – | – |
开销 | 调度、上下文开销等 | 无线程切换,开销低 |
– | – | – |
异步 | 支持 | 不支持 |
– | – | – |
并发支持 | 支持(最大线程池大小) | 支持(最大信号量上限) |
4.7 分布式事务
分布式事务的作用:为了保证各个事务、各个微服务之间的事务的数据一致性。
4.7.1 说说你对2段提交的理解?
两阶段提交协议(Two Phase Commitment Protocol)中,涉及到两种角色
一个事务协调者(coordinator):负责协调多个参与者进行事务投票及提交(回滚)
多个事务参与者(participants):即本地事务执行者
总共处理步骤有两个
(1)投票阶段(voting phase):
协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。
参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障);
(2)提交阶段(commit phase):
收到参与者的通知后,协调者再向参与者发出通知,
根据反馈情况决定各参与者是否要提交还是回滚;
4.7.2 补偿事务(TCC)
TCC 将事务提交分为== Try - Confirm - Cancel 3个操作==。其和两阶段提交有点类似,Try为第一阶段,Confirm - Cancel为第二阶段,是一种应用层面侵入业务的两阶段提交。
操作方法 含义
Try 预留业务资源/数据效验
Confirm 确认执行业务操作,实际提交数据,不做任何业务检查,try成功,confirm必定成功,需保证幂等
Cancel 取消执行业务操作,实际回滚数据,需保证幂等
其核心在于将业务分为两个操作步骤完成。不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
4.7.3 Seata的AT模式
seata中有两种分布式事务实现方案,AT及TCC
AT模式主要关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题
TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题
Seata AT模式是基于XA事务演进而来的一个分布式事务中间件,XA是一个基于数据库实现的分布式事务协议,本质上和两阶段提交一样,需要数据库支持,Mysql5.6以上版本支持XA协议,其他数据库如Oracle,DB2也实现了XA接口
第一阶段:
==Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,==把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。
这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在
基于这样的机制,分支的本地事务便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源
这也是Seata和XA事务的不同之处,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对undolog中对应数据并反解析成sql来达到回滚目的
同时Seata通过代理数据源将业务sql的执行解析成undolog来与业务数据的更新同时入库,达到了对业务无侵入的效果。
第二阶段:
如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成.
如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚
4.8 MQ
4.8.1 MQ主要解决了什么问题?
使用MQ主要可以==解决分布式应用之间的异步通信,==同时还降低了分布式应用之间的耦合。
4.8.2 RabbitMQ有几种队列模式?
7种:
1.简单模式
2.工作者模式
3.广播模式
4.路由模式
5.通配符模式
6.RPC
7.消息确认模式
4.8.3 RabbitMQ中如何方式数据丢失?
1.生产者弄丢了数据
生产者将数据发送到rabbitmq的时候,可能数据就在半路给搞丢了,例如网络问题。
解决方案:RabbitMQ提供transaction和confirm模式来确保生产者不丢消息。transaction机制就是说,发送消息前开启事物(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,
事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())。然而缺点就是吞吐量下降了。
因此,生产上用confirm模式的居多。一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个Ack给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了.如果rabiitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。
2.rabbitmq弄丢了数据
就是rabbitmq自己弄丢了数据,这个你必须开启rabbitmq的持久化,就是消息写入之后会持久化到磁盘,哪怕是rabbitmq自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,rabbitmq还没持久化,自己就挂了,可能导致少量数据会丢失的,但是这个概率较小。
解决方案:
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。
3.消费端弄丢了数据
消费端如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,rabbitmq认为你都消费了,这数据就丢了。
解决方案:启用手动确认模式可以解决这个问题(重试机制)手动确认模式,如果消费者来不及处理就死掉时,没有响应ack时会重复发送一条信息给其他消费者;如果监听程序处理异常了,且未对异常进行捕获,会一直重复接收消息,然后一直抛异常;如果对异常进行了捕获,但是没有在finally里ack,也会一直重复发送消息(重试机制)。
4.8.4 RabbitMQ如何实现延时队列?
Rabbitmq实现延时队列一般而言有两种形式:
==第一种方式:==利用两个特性: Time To Live(TTL)、Dead Letter Exchanges(DLX)[A队列过期->转发给B队列]
第二种方式:利用rabbitmq中的插件x-delay-message
RabbitMQ可以针对队列设置x-expires(则队列中所有的消息都有相同的过期时间)或者针对Message设置x-message-ttl(对消息进行单独设置,每条消息TTL可以不同),来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)
Dead Letter Exchanges(DLX)
RabbitMQ的Queue可以配置x-dead-letter-exchange和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。
x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送
**
**
畅购商城 框架搭建
畅购商城 分布式文件存储
畅购商城 微服务网关限流&鉴权
畅购商场 商品管理
畅购商城 网站首页高可用nginx+lua
畅购商场 数据同步解决方案-canal
畅购商场 商品搜索
畅购商城 商品详情页静态化功能实现
畅购商城 购物车
畅购商城 订单
畅购商城 分布式事务解决方案
畅购商城 秒杀前端+后端
畅购商城 订单+用户认证+微信扫码支付+订单处理