项目基于多个微服务模块实现,使用Nacos做为微服务的注册与发现中心。引入gateway组件作为服务入口的统一管理。使用Open Feign作为服务间通信的组件,基于请求与响应的方式。使用seata组件提供分布式事务的服务。基于ES实现商品的检索等等。
查询商品详情,也就是从商城主页面点击一个商品到展示一个商品的详细信息这一步骤。由于逻辑比较复杂,而且有些数据还需要进行远程调用。
**(获取SKU基本信息 -> 获取SKU图片信息 -> 获取SKU促销信息 -> 获取SPU销售属性 -> 获取规格参数组以及组下参数-> 获取SPU详情)**为了优化这一部分的时间,引入了线程池技术从而多线程异步的方式执行以上任务。
但是异步任务又涉及到一个异步编排的问题,也就是说有些任务的先后顺序是有要求的。可以使用completableFuture
来完成异步编排。
二、登录问题
抽取SSO单点登录模块,一切的登录请求由该模块处理。是基于相同顶级域名的SSO实现。
创建线程池之后,核心线程开始执行任务,核心线程占用完了,其他任务进入阻塞队列,空闲线程开始执行任务。没有空闲线程了并且队列也满了,使用拒绝策略拒绝其他任务。
拒绝策略:抛弃老任务策略、丢弃新任务策略、直接抛异常策略等等
单点登录英文全称Single Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。
普通的登录认证机制:
我们在浏览器(Browser)中访问一个应用,这个应用需要登录,我们填写完用户名和密码后,完成登录认证。这时,我们在这个用户的session中标记登录状态为yes(已登录),同时在浏览器(Browser)中写入Cookie,这个Cookie是这个用户的唯一标识。下次我们再访问这个应用的时候,请求中会带上这个Cookie,服务端会根据这个Cookie找到对应的session,通过session来判断这个用户是否登录。如果不做特殊配置,这个Cookie的名字叫做jsessionid
,值在服务端(server)是唯一的。
同顶级域名下的单点登录:
一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。比如我们有个域名叫做:a.com,同时有两个业务系统分别为:app1.a.com和app2.a.com。我们要做单点登录(SSO),需要一个登录系统(微服务),叫做:sso.a.com。
我们只要在sso.a.com登录,app1.a.com和app2.a.com就也登录了。通过上面的登陆认证机制,我们可以知道,在sso.a.com中登录了,其实是在sso.a.com的服务端的session中记录了登录状态,同时在浏览器端(Browser)的sso.a.com下写入了Cookie。那么我们怎么才能让app1.a.com和app2.a.com登录呢?这里有两个问题:
解决办法:针对cookie跨域问题,将cookie域设置为顶域,即属于a.com。这样所有的子域系统都能访问到顶域的cookie。针对session问题,可以使用Spring-session 解决session共享问题。
spring-session 配合redis 将session 进行统一管理。
方便客户、不需要记住多个 ID 和密码。
默认不可以,但可以设置。
服务端使用使用@CrossOrigin
注解时。只需要在网页端设置跨域 XMLHttpRequest
请求的 withCredentials
属性就可以正常设置和获取跨域 Cookie。
分别压测了动静分离、redis中间件引入前的商城主页面接口。
解决数据库缓存的一致性问题。另外一种解决方式是采用改完数据库删缓存的方式。
canal通过模拟MySQL的从机,从而完成类似于MySQL的主从复制的过程。然后canal将从MySQL读到的数据同步到redis、ES中。
后面分布式事务中有详细讲解。
2pc,后面分布式事务中有详细讲解。
其中N为CPU的核心数。
可以依靠 Open Feign 组件内部封装的Ribbon实现提供相同服务的微服务的负载均衡。
@FeignClient("mall-coupon") // 标识了要去调用的那个微服务
public interface CouponFeignService {
@PostMapping("/coupon/spubounds/save")
R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);
@PostMapping("/coupon/skufullreduction/saveinfo")
R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}
在使用@FeignClient
注解的时候 是默认使用了Ribbon进行客户端的负载均衡的,默认的是随机的策略,那么如果我们想要更改策略的话,需要修改消费者yml
中的配置,如下:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule #配置规则 最空闲连接策略
加第三方登录(微博、QQ等)功能。完善SSO功能。
采用分库分表的方式设置数据库。为每一个微服务设置独立的数据库。
分布式ID的特点:全局唯一性、递增性、高可用性。
常见解决方案:
UUID、雪花算法、UidGenerator、Leaf
雪花算法概要:SnowFlake
是Twitter公司采用的一种算法,目的是在分布式系统中产生全局唯一且趋势递增的ID。SnowFlake
算法在同一毫秒内最多可以生成多少个全局唯一ID呢: 同一毫秒的ID数量 = 1024 X 4096 = 4194304。雪花算法的实现主要依赖于数据中心ID和数据节点ID这两个参数。
微服务的特点:技术异构性、隔离性、可扩展性、易于优化性。
用户往购物车中添加商品并不会直接操作数据库,而是通过操作redis或者cookie然后最终通过redis的持久化机制存储到MySQL中。所以频繁的添加购物车不会有问题。
双写模式: 我们采取MySQL作为主要的数据存储,利用MySQL的事务特性维护数据一致性,使用ElasticSearch进行数据汇集和查询,此时es与数据库的同步方案就尤为重要。
保证es与数据库的同步方案:
1、首先添加商品入数据库,添加商品成功后,商品入ES,若入ES失败,将失败的商品ID放入redis的缓存队列(或MQ),且失败的商品ID入log文件(若出现redis挂掉,可从日志中取异常商品ID然后再入ES),
task任务每秒刷新一下redis缓存队列,若是从缓存队列中取到商品ID,则根据商品ID从数据库中获取商品数据然后入ES。
canal组件控制一致性
若入ES失败,将失败的商品ID放入redis的缓存队列(或MQ),且失败的商品ID入log文件(若出现redis挂掉,可从日志中取异常商品ID然后再入ES),
task任务每秒刷新一下redis缓存队列,若是从缓存队列中取到商品ID,则根据商品ID从数据库中获取商品数据然后入ES。
如果数据库写失败了怎么办?ES写失败了怎么办?
若入ES失败,将失败的商品ID放入redis的缓存队列(或MQ),且失败的商品ID入log文件(若出现redis挂掉,可从日志中取异常商品ID然后再入ES),
task任务每秒刷新一下redis缓存队列,若是从缓存队列中取到商品ID,则根据商品ID从数据库中获取商品数据然后入ES。