此电商项目属于B2C模式的线上商城,支持用户在线浏览商品,在线搜索商品,并且可以将喜欢的商品加入购物车从而下单购买商品,同时支持线上支付,支付模式支持支付宝、微信、银联支付。用户还可以参与低价商品秒杀。
畅购商城采用了微服务架构,微服务技术采用了SpringCloud技术栈,各个微服务站点基于SpringBoot构建,并采用SpringCloud Gateway将各个微服务的功能串联起来,形成一套套系统,同时在微服务网关Gateway中采用过滤和限流策略,实施对微服务进行保护和权限认证操作。项目采用了SpringSecurity OAuth2.0解决了各个微服务之间的单点登录和用户授权。采用了当前非常热门的Seata来解决微服务与微服务之间的分布式事务。采用了Elasticsearch解决了海量商品的实时检索。数据存储采用了MySQL,并结合Canal实现数据同步操作,利用Redis做数据缓存操作。各个微服务之间采用RabbitMQ实现异步通信。我们采用了OpenResty集成的Nginx来控制微服务最外层的大量并发,利用Keepalived+Nginx来解决Nginx单点故障问题。
购物车分为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)防止用户长时间不支付,在订单系统中,还编写一个延时队列监听,半小时后,能监听到订单信息,并在数据库中检查订单是否已经支付,如果已经支付,则无需处理,如果未支付,则需要关闭微信支付,同时修改订单状态和商品状态。
问题说明:
支付中如果出现网络故障,而没有收到微信服务的支付装填响应没怎么处理?
微信服务器提供了一个根据订单号查询支付信息的方法,可以调用该方法查询支付状态信息。
微信支付一个订单付款两次问题处理
1.如何解决秒杀中重复排队问题?
2.如何实现并发削峰操作?
3.如何防止超卖问题?
4.如何实现超时订单回滚?
秒杀中的常见问题的解决
FastDFS是一套分布式文件管理系统,为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
1.Tracker:
负责任务调度,负载均衡管理以及Storage的注册中心功能
2.Storage(操作文件):
负责文件上传和下载以及文件的删除等管理功能
1.不支持文件分片,FastDFS不适合大文件存储
2.同步机制不支持文件正确性校验,降低了系统的可用性
Nginx介绍:
Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like 协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。
OpenResty 简单理解成 就相当于封装了nginx,并且集成了LUA脚本,开发人员只需要简单提供了对应的Lua模块就可以实现相关的逻辑,而不再像之前,还需要在nginx中自己编写lua的脚本,再进行调用了。
OpenResty可以 快速构造出足以胜任 10K 以上并发连接响应的超高性能 Web 应用系统。
upstream:负载均衡池,主要实现多个节点集群,并且能实现负载均衡策略配置
负载均衡策略:
ip_hash:哈希原则
weight:权重
轮询:默认
nginx提供两种限流的方式:
1.一是控制速率 控制速率的方式之一就是采用漏桶算法。
2.二是控制并发连接数
漏桶算法实现控制速率限流:
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:
动静分离, 使用Nginx将动态请求和静态请求分离开来, 减少不必要的请求消耗, 同时能减少请求的延时。
静态数据配置:
//所有图片|静态页|css|js等 请求使用 http://source.itheima.com
//所有的请求将直接使用Nginx在 /usr/local/server/static下面去找对应的静态资源,而不需要经过Nginx
server {
listener 80;
server_name source.itheima.com;
location / {
root /usr/local/server/static;
}
}
动态请求配置:
//所有非静态资源请求 使用http://www.itheima.com
//所有请求会直接路由给 http://127.0.0.1:8080的服务器处理
server {
listener 80;
server_name www.itheima.com
location / {
proxy_pass http:127.0.0.1:8080;
}
}
canal可以用来监控数据库数据的变化,从而获得新增数据,或者修改的数据。
原理相对比较简单:
canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送ump协议
mysql master收到dump请求,开始推送binary log给slave(也就是canal)
canal解析binary log对象(原始为byte流)
canal在项目中,主要用于做数据增量同步操作,可以将数据同步到Redis、其他MySQL、Elasticsearch等,在我们项目中主要实现了广告缓存同步,网站公告,商品数据,击穿缓存更新,商品缓存数据更新,Elasticsearch索引库数据增量更新。
项目中,用户请求由Nginx进行拦截处理,再将请求路由给微服务网关,微服务网关再将请求路由给其他微服务,通过微服务网关可以整合相关功能,所以项目中微服务网关不止一套(集群)。在微服务网关这里,还可以实现限流、鉴权相关操作。
优点如下:
安全。只有网关系统对外进行暴露,微服务可以隐藏在内网,通过防火墙保护。
易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
减少了客户端与各个微服务之间的交互次数,易于统一授权。
在微服务网关中,我们使用令牌桶算法实现限流。
令牌桶算法是比较常见的限流算法之一,大概描述如下:
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流
**JSON Web Token(JWT)**是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
简单来说:JWT令牌是一段JSON字符串,主要用于封装用户身份相关信息,可在各个微服务之间进行传递,从而识别用户的身份信息,通过它可以解决微服务中单点登录问题。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{“typ”:“JWT”,“alg”:“HS256”}
载荷(playload)
载荷就是存放有效信息的地方。
签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret 秘钥
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
令牌:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJObzAwMDEiLCJpYXQiOjE1NjkxNTg4MDgsInN1YiI6IuS4u-mimCIsImlzcyI6Ind3dy5pdGhlaW1hLmNvbSIsImV4cCI6MTU2OTE1ODgyMywiYWRkcmVzcyI6IuS4reWbvSIsIm1vbmV5IjoxMDAsImFnZSI6MjV9.lkaOahBKcQ-c8sBPp1Op-siL2k6RiwcEiR17JsZDw98
如果令牌被盗,只要该令牌不过期,任何服务都可以使用该令牌,有可能引起不安全操作。我们可以在每次生成令牌的时候,将用户的客户端信息获取,同时获取用户的IP信息,然后将IP和客户端信息以MD5的方式进行加密,放到令牌中作为载荷的一部分,用户每次访问微服务的时候,要先经过微服务网关,此时我们也获取用户客户端信息,同时获取用户的IP,然后将IP和客户端信息拼接到一起再进行MD5加密,如果MD5值和载荷不一致,说明用户的IP发生了变化或者终端发生了变化,有被盗的嫌疑,此时不让访问即可。这种解决方案比较有效。
当然,还有一些别的方法也能减少令牌被盗用的概率,例如设置令牌超时时间不要太长。
OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本。OAuth2.0提供了4种授权模式,分别为授权码模式授权、密码模式授权 、隐式模式授权、客户端模式授权,其中常用的是授权码模式授权和密码模式授权。
1、客户端请求第三方授权(微信:提供授权码,发放令牌)
2、用户(资源拥有者)同意给客户端授权
3、客户端获取到授权码,请求认证服务器申请 令牌
4、认证服务器向客户端响应令牌
5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
6、资源服务器返回受保护资源
1.用户登录成功后,会将令牌信息存入到cookie中(一般建议存入到头文件中)
2.用户携带Cookie(或者Header)中的令牌访问微服务网关
3.微服务网关先获取头文件中的令牌信息,如果Header中没有Authorization令牌信息,则取参数中找,参数中如果没有,则取Cookie中找Authorization,最后将令牌信息封装到Header中,并调用其他微服务
4.其他微服务会获取头文件中的Authorization令牌信息,然后匹配令牌数据是否能使用公钥解密,如果解密成功说明用户已登录,解密失败,说明用户未登录
微服务与微服务之间实现认证,只需要将用户传递的令牌Authorization传递给其他微服务即可。如果微服务之间相互调用采用的是Feign模式,可以创建一个拦截器(实现接口:RequestInterceptor ),每次执行请求之间,将令牌添加到头文件中即可传递给其他微服务。
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();
}
}
}
微服务之间相互调用,如果使用Feign调用,如果开启feign熔断,默认采用的是线程,feign调用和请求的线程不属于同一个线程,无法获取请求的线程数据,会造成空指针异常。
解决方案:hystrix隔离策略换为SEMAPHORE(信号量隔离)
隔离区别:
比较项 | 线程池隔离 | 线程池隔离 |
---|---|---|
线程 | 与调用线程非相同线程 | 与调用线程相同(jetty线程) |
– | – | – |
开销 | 调度、上下文开销等 | 无线程切换,开销低 |
– | – | – |
异步 | 支持 | 不支持 |
– | – | – |
并发支持 | 支持(最大线程池大小) | 支持(最大信号量上限) |
分布式事务的作用:为了保证各个事务、各个微服务之间的事务的数据一致性。
两阶段提交协议(Two Phase Commitment Protocol)中,涉及到两种角色
一个事务协调者(coordinator):负责协调多个参与者进行事务投票及提交(回滚)
多个事务参与者(participants):即本地事务执行者
总共处理步骤有两个
(1)投票阶段(voting phase):
协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。
参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障);
(2)提交阶段(commit phase):
收到参与者的通知后,协调者再向参与者发出通知,
根据反馈情况决定各参与者是否要提交还是回滚;
TCC 将事务提交分为== Try - Confirm - Cancel 3个操作==。其和两阶段提交有点类似,Try为第一阶段,Confirm - Cancel为第二阶段,是一种应用层面侵入业务的两阶段提交。
操作方法 含义
Try 预留业务资源/数据效验
Confirm 确认执行业务操作,实际提交数据,不做任何业务检查,try成功,confirm必定成功,需保证幂等
Cancel 取消执行业务操作,实际回滚数据,需保证幂等
其核心在于将业务分为两个操作步骤完成。不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
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 并执行,以完成分支的回滚
Seata参考地址
使用MQ主要可以解决分布式应用之间的异步通信,同时还降低了分布式应用之间的耦合。
7种:
1.简单模式
2.工作者模式
3.广播模式
4.路由模式
5.通配符模式
6.RPC
7.消息确认模式
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,也会一直重复发送消息(重试机制)。
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发送