微服务及分布式简单整理

目录

SOA、分布式与微服务的区别

服务的拆分

常用的微服务组件

简述ZAB协议     

服务注册与发现--zookeeper

zk的数据模型和节点类型     

简述zk的命名服务、配置管理、集群管理

Zookeeper watch机制     

什么是服务降级、什么是熔断   

什么是Hystrix?简述实现机制     

高并发场景下如何实现系统限流 

springcloud核心组件及作用

分布式锁

分布式事务解决方案     

Seata的实现原理 

常见实现接口的幂等性     

Dubbo的整体架构设计及分层     

SpringCloud 和 Dubbo对比     

负载均衡算法、类型     

分布式架构下的session共享问题

分布式系统中常用的缓存方案有哪些     

定时任务实现原理     

Nacos注册中心原理

Nacos配置中心原理

服务网关可以做什么 ​

微服务架构排错

SOA、分布式与微服务的区别

  • 分布式:将一个项目拆分成了多个模块,水平按照层次(表示层、业务层、数据层)垂直拆(按不同业务块拆),并将这些模块分开部署;
  • SOA是一种面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用
  • 微服务:细粒度的垂直拆分。分布式属于微服务

服务的拆分

 1,抽取公共的基础服务(短信,邮件,文件管理)                                                                               2,以业务为边界,拆分服务                                                                                                               3,管理多个服务-- 注册中心                                                                                                             4,服务之间需要通信--通信的方式                                                                                                               1.同步调用Dubbo, RPC的方式,底层采用Netty来实现,基于TCP建立的是长连接
注意: BIO, NIO只是一种网络通信模式BIO:为每个连接创理个纯程NIO: -个往程服务多个连接,有弊端,当连接数太多,性能下降Netty:封装了NIO, 在它的基础上,添加主从处理组,BossGraup, WorkerGroup  编程模型: Reactor (反应堆)思想,NIO.Netty是 种实现SpringCloud, restul http的方式 短链接的方式 性能角度:Dubbo > SpringCloud调用方需要等待执行方的处理结果                                                                                                                                        
         2.异步通知MQ,调用方无需等待执行方的处理结果

常用的微服务组件

简述ZAB协议     

ZAB协议是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议,实现分布式数据一致性 。所有客户端的请求都是写入到Leader进程中,然后,由Leader同步到其他节点,称为Follower。在集群数据同 步的过程中,如果出现Follower节点崩溃或者Leader进程崩溃时,都会通过Zab协议来保证数据一致性。

ZAB协议包括两种基本的模式:崩溃恢复和消息广播。   

消息广播:     集群中所有的事务请求都由Leader节点来处理,其他服务器为Follower, Leader将客户端的事务请求转换为事     务Proposal,并且将Proposal分发给集群中其他所有的Follower. 完成广播之后,Leader 等待Follwer 反馈,当有过半数的Follower反馈信息后,Leader 将再次向集群内    Follower广播Commit信息,Commit 信息就是确认将之前的Proposal 提交。     

Leader节点的写入是一个两步操作, 第-步是广播事务操作,第二步是广播提交操作,其中过半数指的是反馈的     节点数>=N/2+1, N是全部的Follower节点数量。     

崩溃恢复:     

●初始化集群,刚刚启动的时候 

●Leader 崩溃,因为故障宕机     

●Leader 失去了半数的机器支持,与集群中超过-半的节点断连     

此时开启新-轮Leader选举,选举产生的Leader会与过半的Follower进行同步,使数据一致, 当与过半的机器     同步完成后,就退出恢复模式,然后进入消息广播模式   

 整个ZooKeeper集群的一致性保证就是在上面两个状态之前切换,当Leader服务正常时,就是正常的消息广播模式;当Leader不可用时,则进入崩溃恢复模式,崩溃恢复阶段会进行数据同步,完成以后,重新进入消息广播阶段。.     

Zxid是Zab协议的一个事务编号,Zxid 是一个64位的数字,其中低32位是一个简单的单调递增计数器,针对客户端每一个事务请求, 计数器加1;而高32位则代表Leader周期年代的编号。     Leader周期( epoch),可以理解为当前集群所处的年代或者周期,每当有一个 新的Leader选举出现时,就会从这个Leader服务器上取出其本地日志中最大事务的Zxid,并从中读取epoch值,然后加1,以此作为新的周期ID。高32位代表了每代Leader的唯一性, 低32位则代表了每代Leader中事务的唯一性。     

zab节点的三种状态:       

  • following:服从leader的命令   
  • leading:负责协调事务     
  • election/looking:选举状态

服务注册与发现--zookeeper

znode树状结构、watch机制、本地缓存服务列表

zk的数据模型和节点类型     

数据模型:树形结构     

zk维护的数据主要有:客户端的会话(session) 状态及数据节点(dataNode) 信息。     

zk在内存中构造了个DataTree的数据结构,维护着path到dataNode的映射以及dataNode间的树状层级关系。 为了提高读取性能,集群中每个服务节点都是将数据全量存储在内存中。所以, zk最适于读多写少且轻量级数据的应用场景。     

数据仅存储在内存是很不安全的,zk采用事务日志文件及快照文件的方案来落盘数据,保障数据在不丢失的情况下能快速恢复。     

树中的每个节点被称为一Znode     

Znode兼具文件和目录两种特点。可以做路径标识,也可以存储数据,并可以具有子Znode。具有增、删、改、查等操作。   

Znode具有原子性操作,读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。另外,每一个节点都拥有自己的ACL(访问控制列表),这个列表规定了用户的权限,即限定了特定用户对目标节点可以执行的 操作     

Znode存储数据大小有限制。每个Znode的数据大小至多1M,常规使用中应该远小于此值。(因为zk主要做服务协调,不会太多,也能节省带宽,保持轻量及效率)   

Znode通过路径引用,如同Unix中的文件路径。 具有一个固定的根节点 (/) ,可以在根节点下创建子节点,并在子节点下继续创建下一级节点。每一层级用/ 隔开,且只能用绝对路径(get/work/task1)的方式查询ZK节点路径必须是绝对的,因此他们必须由斜杠字符来开头。除此以外,他们必须是唯一的, 也就是说每一个路径只有一 个表示, 因此这些路径不能改变。在ZooKeeper中,路径由Unicode字符串组成,并且有一些限制。字符串"/zookeeper"用以保存管理信息, 比如关键配额信息 。

持久节点: 一旦创建、该数据节点会一直存储在zk服务器 上、即使创建该节点的客户端与服务端的会话关闭了、该节点也不会被删除 除非显式调用delete函数进行明除操作。创建有序节点的时候,ZK服务器会自动使用一个单调递增的数字作为后缀, 追加到创建的节点后边。例如一个窖户端创建了一个路径为 works/task-的有序节点,那么ZooKeeper将会生成一个序s并迫加到该节点的路径 后,最后该节点的路径为works/task-1。     

节点内容: 一个二进制数组(byte data])。用来存储节点的数据、ACL访问控制、子节点数据(因为临时节点不允许有子节点,所以其子节点字段为nul) . 记录自身状态信息的stat,stat +节点路径可以查看状态信息。

临时节点:当创建该节点的客户端会话因超时或发生异常而关闭时、该节点也相应的在zk上被删除。(分布式锁广泛使用:解决死锁问题,数据库唯一索引也可以解决但是客户端挂掉的话此锁不能及时删除,定时删除操作成本高。zk中临时节点zk发现客户端不在了会主动删除,不会出现死锁问题)     

有序节点:不是一种单独种类的节 点而是在持久节点和临时节点的基础上、增加了一个节点有序的性质。

简述zk的命名服务、配置管理、集群管理

命名服务: 通过指定的名字来获取资源或者服务地址(注册中心)。Zookeeper可以创建一 个全局唯一 的路径, 这个路径就可以作为一个名字。被命名的实体可以是集群中的机器,服务的地址,或者是远程的对象等。一些分布式服务框架 (RPC、RMI)中的服务地址列表,通过使用命名服务,客户端应用能够根据特定的名字来获取资源的实体、服务地址和提供者信息等     

配置管理: 实际项目开发中,经常使用.properties或者xml需要配置很多信息, 如数据库连接信息、fpst地址端口等等。 程序分布式部署时,如果把程序的这些配置信息保存在zk的znode节点下,当你要修改配置,即znode会发生变化时,可以通过改变zk中某个目录节点的内容,利用watcher通知给各个客户端,从而更改配置。     

集群管理: 集群管理包括集群监控和集群控制,就是监控集群机器状态,剔除机器和加入机器。zookeeper可以方 便集群机器的管理,它可以实时监控znode节点的变化,一旦发现有机器挂了,该机器就会与zk断开连接,对应的临时目录节点会被删除,其他所有机器都收到通知。新机器加入也是类似。

Zookeeper watch机制     

客户端,可以通过在znode.上设置watch,实现实时监听znode的变化Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端(发生了什么变化是需要去查看的)

  • 父节点的创建,修改,删除都会触发Watcher事件。     
  • 子节点的创建,删除会触发Watcher事件。     

一次性: - 旦被触发就会移除 (减少不必要的通知),再次使用需要重新注册, 因为每次变动都需要通知所有客户端,一 -次性可以减轻压力,3.6.0默认持久递归,可以触发多次     

轻量:只通知发生了事件,不会告知事件内容,减轻服务器和带宽压力

Watcher机制包括三个角色:客户端线程、客户端的WatchManager以及ZooKeeper服务器     

  1. 客户端向ZooKeeper服务器注册一个Watcher监听,     
  2. 把这个监听信息存储到客户端的WatchManager中(把watcher对象存起,监听信息还会往zkServer上注册类似map的节点)     
  3. 当ZooKeeper中的节点发生变化时,会通知客户端,客户端会调用相应Watcher对象中的回调方法。watch回调是串行同步的 

什么是服务降级、什么是熔断   

降级是解决系统资源不足和海量业务请求之间的矛盾     

在暴增的流量请求下,对一些非核心流程业务、非关键业务,进行有策略的放弃,以此来释放系统资源,保证核心业务的正常运行,尽量避免这种系统资源分配的不平衡,打破二八策略,让更多的机器资源,承载主要的业务请求。服务降级不是一个常态策略 ,而是应对非正常情况下的应急策略。服务降级的结果,通常是对一些业务请求,返回一个统-的结果,可以理解为是一Failover快速失败的策略。一般通过配置中心配置开关实现开启降级     

熔断模式保护的是业务系统不被外部大流量或者下游系统的异常而拖垮。     

如果开启了熔断,订单服务可以在下游调用出现部分异常时,调节流量请求,比如在出现10%的失败后,减少509%6的流量请求,如果继续出现50%6的异常,则减少80%的流量请求;相应的,在检测的下游服务正常后,首先恢复30%6的流量,然后是50%6的流量,接下来是全部流量 

服务降级的预案
在进行降级之前要对系统进行梳理,提前将一些不重要或不紧急的服务或任务进行服务的延迟使用或暂停使用。
看看哪些服务是必须誓死保护,哪些系统是能够丢卒保帅;具体可以参考日志级别设置预案:
一般: 有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
警告:有些服务在一段时间内成功率有波动(如在95-100%之间) , 可以自动降级或人工降级,并发送告警:
错误:可用率低于90%,或者连接池被占用满了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
严重错误:因为特殊原因数据错误了,此时需要紧急人工降级
计算关系:
QPS =并发量1平均响应时间
并发量= QPS●.平均响应时间
根据以上计算关系,我们来预估下单日访问量在1000W需要多大的QPS来支持:
通常情况下,80% 的访问量集中在20%的时间,算一下这 1000w pv实际需要机器达到多少qps才能满足,
qps = (1000w* 0.8)1 (24 * 3600 *0.2)
qps = 462.9
根据压力测试的反馈,单台机子的QPS是多少 ,利用以上结果就可以算出需要几台机器或大致推算出需不需要使用缓存配置
方案一:使用集群服务器不使用缓存服务器
方案二:使用集群服务器同时使用缓存服务器(推荐)
例子:
每秒可以处理的请求数QPS (TPS) : 每秒钟可以处理的请求或者事务的数量。
并发数:系统同-一时候处理的请求数量(事务数)
响应时间: - 般取平均响应时间
QPS (TPS) =并发数/平均响应时间
并发数= QPS*平均响应时间
例子:
一个典型的.上班签到系统,早上8点上班。7点半到8点这30分钟的时间里用户会登录签到系统进行签到。公司员工为1000人, 平均每一个员上登录签到系统的
时长为5分钟。能够用以下的方法计算。
(1) QPS = 1000/(30*60)事务秒
(2)平均响应时间为=5*60秒
(3)并发数= QPS*平均响应时间= 1000/(30*60) *(5*60)=166.7
再看如果者板要求实现多少并发数,在反推出机器需要多少QPS,看是否集群配置。

什么是Hystrix?简述实现机制     

分布式容错框架

  •     阻止故障的连锁反应,实现熔断
  •     快速失败,实现优雅降级.
  •     提供实时的监控和告警     

资源隔离:线程隔离,信号暈隔离     

  • 线程隔离: Hystrix会给每一 个Command分配一个单独的线程池,这样在进行单个服务调用的时候,就可以在独立的线程池里面进行,而不会对其他线程池造成影响     
  • 信号量隔离:客户端需向依赖服务发起请求时,首先要获取一 个信号量才能真正发起调用,由于信号量的数量有限,当并发请求量超过信号量个数时,后续的请求都会直接拒绝,进入fallback流程。 信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。      

熔断和降级:调用服务失败后快速失败     

熔断是为了防止异常不扩散,保证系统的稳定性     

降级:编写好调用失败的补救逻辑,然后对服务直接停止运行,这样这些接口就无法正常调用,但又不至于直接报     错,只是服务水平下降     

  • 通过HystrixCommand或者HystrixObservableCommand将所有的外部系统(或者称为依赖)包装起来,整个包装对象是单独运行在一个线程之中 (这是典型的命令模式)。     
  • 超时请求应该超过你定义的阈值
  • 为每个依赖关系维护一个小的线程池(或信号量) ; 如果它变满了,那么依赖关系的请求将立即被拒绝(fallback),而不     是排队等待。
  • 统计成功,失败(由客户端抛出的异常),超时和线程拒绝。
  • 打开断路器可以在一段时间内停 止对特定服务的所有请求,如果服务的错误百分比通过阈值,手动或自动的关     闭断路器。
  • 当请求被拒绝、连接超时或者断路器打开,I直接执行fallback逻辑。
  • 近乎实时监控指标和配置变化。 

高并发场景下如何实现系统限流 

限流-般需要结合容量规划和压测来进行。当外部请求接近或者达到系统的最大阈值时,触发限流。采取其他的手段进行降级,保护系统不被压垮。常见的降级策略包括延迟处理、拒绝服务、随机拒绝等。     

计数器法:     (在临界值极端情况下会有问题)

  1. 将时间划分为固定的窗口大小,例如1s     
  2. 在窗口时间段内,每来一一个请求, 对计数器加1.     
  3. 当计数署达到设定限制后,该窗1 时间内的之后的请求都被丢并处理。     
  4. 该窗口时间结束后,计数幂清零,从新开始计数。     

滑动窗口计数法:   

  1. 将时间划分为细粒度的区间,每个区间维持一个计数器, 每进入一个请求则将计数器加一。 
  2. 多个区间组成一个时间窗口,每流逝一个区间时间后,则抛弃最老的个区间,纳入新区间,如图中示例的窗口T1变为窗口T2   
  3. 若当前窗口的区间计数器总和超过设定的限制数量,则本窗口内的后续请求都被丢弃。     

漏桶算法:如果外部请求超出当前阈值,则会在容易里积蓄,一直到溢出, 系统并不关心溢出的流量。从出口处限制请求速率,并不存在计数器法的临界问题,请求曲线始终是平滑的。无法应对突发流量,相当于一个空桶+固定处理线程     (对突发的多请求不友好,可能会舍弃很多请求)

令牌桶算法:假设一个大小恒定的桶,这个桶的容量和设定的阈值有关,桶里放着很多令牌,通过一个固定的速率,往里边放入令牌,如果桶满了,就把令牌丢掉,最后桶中可以保存的最大令牌数永远不会超过桶的大小。当有请求进入时,就尝试从桶里取走一个令牌,如果桶是空的,那么这个请求就会被拒绝。

springcloud核心组件及作用

Eureka:服务注册与发现   

注册:每个服务都向Eureka登记自己提供服务的元数据,包括服务的ip地址、端口号、版本号、通信协议等。 eureka将各个服务维护在了一个服务清单中(双层Map, 第一层key是服务名, 第二_层key是实例名,value是服务地址加端口)。同时对服务维持心跳,剔除不可用的服务,eureka集群 各节点相互注册每个实例中都有一样的服务清单。

发现: eureka注册的服务之间调用不需要指定服务地址,而是通过服务名向注册中心咨询,并获取所有服务实例清单(缓存到本地),然后实现服务的请求访问。

采用多级缓存形式解决读写高并发冲突问题
在拉取注册表的时候:

  • 首先从ReadOnlyCacheMap(只读缓存)里查缓存的注册表。
  • 若没有,就找ReadWriteCacheMap里缓存的注册表。
  • 如果还没有,就从内存中获取实际的注册表数据。

在注册表发生变更的时候:

  • 会在内存中更新变更的注册表数据,同时过滤掉ReadWriteCacheMap(读写缓存).
  • 此过程不会影响ReadOnlyCacheMap提供查询注册表。
  • 默认每30秒Eureka Server会将ReadWriteCacheMap更新到ReadOnlyCacheMap里
  • 默认每180秒Eureka Server会将ReadWriteCacheMap里是否数据失效
  • 下次有服务拉取注册表,又会从内存中获取最新的数据了,同时填充各级缓存。

Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台(被调用方的服务地址有多个),Ribbon也是通过发起http请求,来进行的调用,只不过是通过调用服务名的地址来实现的。虽然说Ribbon不用去具体请求服务实例的ip地址或域名了,但是每调用一个接口都还要手动去发起Http请求。核心原理是一个拦截器

@RestController
	public class ConsumerController {
	@Autowired
	RestTemplate restTemplate;
	@GetMapping("/ribbon-consumer ")
	pub1ic string he11oConsumler (){
	return
	restTemp1ate . getForEntity("http://exampleservice/index" ,String . class). getBody();}}

Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求,简化服务间的调     用,在Ribbon的基础上进行了进-步的封装。单独抽出了-个组件, 就是Spring Cloud Feign。在引|入Spring     Cloud Feign后,我们只需要创建一个接口并用注解的方 式来配置它,即可完成对服务提供方的接口绑定。调用远程就像调用本地服务一样

    @RestController
	pub1ic class UserController {
	    @GetMapping("/getUser")
	    public string getUser(){
	        List<string> list = new ArrayList<>O;
	        1ist . add("张三"); 
	        string json = JSON . toJsoNstring(1ist);
	        return json;
	    }
	}

	@FeignClient(name = "user")
	pub1ic interface Userclient {
	    @GetMapping("/getUser")
	    String getUser();
	    @RestController
	    public c1ass TestController {
            @Resource
	        userclient userclient ;
    
	        @RequestMapping(" /test")
	        public string testO){
	            string user = userclient. getuser(); 
	            return user ;
	}

Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,通过统计接口超时次数返回默认值,实现服务熔断和降级(A服务调B,B调C,假如A的并发量很大请求超时会把整个并发量向后传导,导致BC的并发量也很大,就极有可能因为A这一个接口把整条链路全部压垮,或者后面的资源处理有问题导致A无法继续,Hystrix就起到熔断作用保护现有健康服务 )对于没有即时处理的请求先响应并记录日志,后续读取日志记录异步处理。

Zuul:如果前端、移动端要调用后端系统,统-从Zuul网关进入, 由Zuu网关转发请求给对应的服务,通过与Eureka进行整合,将自身注册为Eureka'下的应用,从Eureka 下获取所有服务的实例,来进行服务的路由。Zuul还提供了一套过滤器机制, 开发者可以自己指定哪些规则的请求需要执行校验逻辑,只有通过校验逻辑的请求才会被路由到具体服务实例上,否则返回错误提示。也可选Gateway

分布式id生成方案     

uuid .     

  1. 当前日期和时间     时间戳     
  2. 时钟序列。     计数器     
  3. 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。     

优点:代码简单,性能好(本地生成,没有网络消耗),保证唯一。(相对而言,重复概率极低可以忽略)     

缺点:

  • 每次生成的ID都是无序的,而且不是全数字,且无法保证趋势递增对数据库聚簇索引不友好。
  • UUID生成的是字符串,字符串存储性能差,查询效率慢,写的时候由于不能产生顺序的append操作,需要进行insert操作,导致频繁的页分裂,这种操作在记录占用空间比较大的情况下,性能下降比较大,还会增加读取磁盘次数
  • UUID长度过长,不适用于存储,耗费数据库性能。ID无-定业务含义,可读性差。
  • 有信息安全问题,有可能泄露mac地址   

数据库自增序列     

单机模式:     

优点:

  • 实现简单,依靠数据库即可,成本小。   
  • ID数字化,单调白增,满足数据库存储和查询性能。
  • 具有一定的业务可读性。(结 合业务code)     

缺点:     

  • 强依赖DB,存在单点问题,如果数据库宕机,则业务不可用。   
  • DB生成ID性能有限,单点数据库压力大,无法扛高并发场景。     
  • 信息安全问题,比如暴露订单量,ur1查询改一下id查到别人的订单     

数据库高可用:多主模式做负载,基于序列的起始值和步长设置,不同的初始值,相同的步长,步长大于节点数     

优点:     解决了ID生成的单点问题,同时平衡了负裁。     

缺点:     

  • 系统扩容困难:系统定义好步长之后,增加机器之后调整步长困难。   
  • 数据库压力大:每次获取一- 个ID都必须读写-次数据库。   
  • 主从同步的时候:电商下单->支付insert master db select数据 ,因为数据同步延迟导致查不到这个数措。加cache (不是最好的解决方式)数据要求比较严连的话查master主库。        

雪花算法     

生成:一个64bit的整性数字,第一位符号位固定为0,41位时间戳,10位workId(机器id), 12位序列号(自增长的)位数可以有不同实现     

优点:     

每个毫秒值包含的ID值很多,不够可以变动位数来增加,性能佳(依赖workId的实现)。 时间戳值在高位,中问是固定的机器码,自增的序列在低位,整个ID是 趋势递增的。能够根据业务场景数据库节点布置灵活挑战bit位划分,灵活度商。     

缺点:     

强依赖于机器时钟,如果时钟回拨,会导致重复的ID生成,所以一般基于此的算法发现时钟回拨,都会抛异常处理,阻止ID生成,这可能导致服务不可用。 这种情况少到可以忽略,不过可以用百度开源的分布式唯一ID生成器UidGenerator。或美团点评Leaf分布式ID生成系统

分布式锁

保证锁是共享的第三方资源需要这个锁独立于每一个服务之外,而不是在服务里面。上锁1解锁 0  

数据库的方式:利用主键冲突控制一次只有一个线程能获取锁,非阻塞需要自己实现等待、不可重入(每次都要获取锁)、单点、失效时间(数据库不好支持key的失效时间)t_ lock     id lock(0)  数据库是磁盘操作(IO)效率低

  • 上锁:将数值修改为1 
  • 解锁:将数值修改为0     

Redis方式: 内存操作单线程处理网络请求,不需要考忠并发安全性(适合做分布式锁也是现在用的较多的一种)所有服务节点设置相同的key,返回为0、则锁获取失败    setnx:     key不存在,就设置成功,否则,设置失败     不适合集群模式(AP的对一致性欠缺,主从数据同步不及时)

  • 上锁:成功执行setnx key value     
  • 解锁: delete key     
  • 避免死锁:需要设置过期时间expire key timeout
  • 注意需要将两个操作(设置锁和锁的有效期)变成一个原子操作(4.0之前Redis + lua脚本。lua脚本是帮助我们扩充redis指令的;4.0之后直接使用现成的指令,在设置数据的时候同时设置时间)
  • 避免无锁:(上锁并设置了过期时间后业务逻辑执行时间过长,锁已过期被删除,下一个服务上锁被之前执行完的服务释放,导致无锁)---释放锁的时候检查这把锁是否当前线程的(可以将锁的值设置为UUID)

问题:     

  1. 早期版本没有超时多数,需要单独设置(expire命令),存在死锁问题(中途宕机)   
  2. 后期版本提供加锁与没置时间原子操作(set(NX,time)),但是存在任务超时,锁自动释放,导致并发问题,加锁与释放锁不是同一线程问题 (可以set的时候将value存为当前线程唯一标识)

删除锁:判断线程唯一标志,再删除     

可重入性及锁续期没有实现,通过redisson解决(类似AQS的实现,看门狗监听机制给key设置一个监听器,监听这个任务,判断是否执行完,没有执行完就把key的过期时间延长)   

redlock:意思的机制都只操作单节点、即使Redis通过sentinel保证高可用, 如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况(redis同步 设置可能数据丢失)。redlock从多个节点申请锁, 当一半以上节点获取成功、锁才算获取成功,redission有相应的实现 

zookeeper:      zk通过临时节点,解决了死锁的问题,一 旦客户端获取到馈之后实然挂掉(session连接断开),那么这个临时节点就公自动删除掉(不会造成死锁了),其他客户端自动获取锁。临时颗序节点解决惊群效应。以节点作为锁,创建成功,表示获取锁成功     集群的话zk方式更可靠

  • 上锁:创建节点lock     
  • 解锁:删除节点lock     
  • 避免死锁:节点为临时节点类型,客户端如果挂掉,会自动删除   
  • 羊群(惊群)效应:  一旦锁失效后假设有上百个客户端都争抢这把锁,这时候称为羊群效应,其实只让一个客户端抢到即可。用时序节点,节点有序号,按序号获取锁 

分布式事务解决方案     

基于XA规范:分布式事务规范,定义了分布式事务模型     不同的数据库都要遵循XA规范

  • 四个角色:事务管理器(协调者TM。每个数据库将各自状态上报给TM,TM根据数据库状态决定一起提交/回滚)、资源管理器(参与者RM),应用程序AP,通信资源管理器CRM     
  • 全同事务: -个横跨多个数据库的事务,要么全部提交、要么全部回滚     
  • JTA事务是java对XA规范的实现,对应JDBC的单库事务     

两阶段协议: 

 第一阶段(prepare) : 每个参与者执行本地事务但不提交,进入ready状态,并通知协调者已经准备就绪。     

第二阶段(commit)当协调者确认每个参 与者都ready后,通知参与者进行commit操作;如果有参与者fail     则发送rollback命令,各参与者做回滚。   

 问题:   

  • 单点故障:一旦事务管理器TM出现故障,整个系统不可用(参与者都会阳塞住)     
  • 数据不一致:在阶段二,如果事务管理器只发送了部分commit消息,此时网络发生异常,那么只有部分参与者接收到commit消息,也就是说只有部分参与者提交了事务,使得系统数据不致。  
  • 响应时间较长:参与者和协调者资源都被锁住,提交或者回滚之后才能释放   
  • 不确定性:当协事务管理器发送commit之后,并且此时只有一个参与者收到了commit, 那么当该参与者与事务管理器同时宕机之后,重新选举的事务管理路无法确定该条消息是否提交成功

三阶段协议:主要对两阶段的优化,解决了2PC单点故障的问题,但是性能问题和不一致问题仍然没有根本解决

 引入了招时机制解决参与者阻塞的问题,超时后本地提交, 2pc只有协调者有超时机制   

  • 第一阶段: CanCommit阶段, 协调者询问事务参与者。是否有能力完成此次事务。

                如果都返回yes,则进入第二阶段   

                有一个返回no或等待响应超时,则中断事务,并向所有参与者发送abort请求     

  • 第二阶段: PreCommit阶段, 此时协调者会向所有的参与者发送PreCommit请求,参与者收到后开始执行事务操作。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈"Ack"表示我已经准备好提交了,并等待协调者的下一-步指令。   
  • 第三阶段: DoCommit阶段, 在阶段二 中如果所有的参与者节点都返回了Ack, 那么协调者就会从“预提交状态转变为“提交状态"。然后向所有的参与5者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈"Ack"消息,协调者收到所有参与者的Ack消息后完成事务。相反,如果有一个参 与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。

由于是强一致性,资源需要在事务内部等待,性能影响较大,吞吐率不高,不适合高并发与高性能的业务场景。在JavaEE平台下,WebLogic、Webshare等主流商用的应用服务器提供了JTA的实现和支持。而在Tomcat下是没有实现的,这就需要借助第三方的框架Jotm、Automikos等来实现,两者均支持spring事务整合。 

基于事务补偿机制的:TCC (基于业务层面) : Try. Confirm. Cancel     

针对每个操作,都要注册一个 与其对应的确认和补偿(撤销)操作     

Try操作做业务检查及资源预留,Confirm做业务确认操作, Cancel实现一 个与Try相反的操作既回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若tny操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confrm/Cancel操作若执行失败, TM会进行重试。   

TCC模型对业务的侵入性较强,改造的难度较大,每个操作都需要有try、confirm. cance1三个接口实现confirm和cancel接口还必须实现幕等性。适合用于满送积分优惠券等场景  

基于消息队列的事务消息:   

  • 发送prepare消息到消息中间件   
  • 发送成功后,执行本地事务      
    • 如果事务执行成功,则commit, 消息中间件将消息下发至消费端(commit前, 消息不会被消费)
    • 如果事务执行失败,则回滚,消息中间件将这条prepare消息删除     
  • 消费端接收到消息进行消费,如果消费失败,则重试      

适合注册、登录送xxx的场景

Seata的实现原理 

在应用中Seata整体事务逻辑基于两阶段提交的模型.核心概念包含三个角色:

  • TC:事务协调者,即独立运行的seata-server,用于接收事务注册,提交和回滚。
  • TM:事务发起者。用来告诉TC全局事务的开始。提交,回滚。
  • RM:事务资源,控制分支事务,每一个RM都会作为一个分支事务注册在TC。

AT(Auto Transaction)模式 默认常用
第一阶段 Seate会拦截“业务SQL”:解析SQL语义,找到“业务SQL"要更新的业务数据,在业务数据被更新前,将其保存成"before image”前置镜像-->执行“业务SQL"更新业务数据,在业务数据更新之后-->其保存成"after image" 后置镜像,最后生成行锁。
以上操作全部在一个数据库事务内完成, 这样保证了一阶段操作的原子性。
过程:
● TM向TC申请开启一个全局事务,全局事务创建并生成一个全局唯一的XID。
● XID在微服务调用链路的上”下文中传播。
假设运行: update product set name = 'GTS where name= 'TXC: /l id=1

  1. 解析SQL:得到SQL的类型(UPDATE) ,表(product),条件(where name = 'TXC)等相关的信息。
  2. 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
    select * from produc where name = 'TXC'镜像前数据
  3. 执行业务SQL:更新这条记录的name为'GTS'。
  4. 查询后镜像:根据前镜像的结果,通过主键定位数据。select * from produc where name = 'TXC'
  5. 插入回滚日志:把前后镜像数据以及业务SQL相关的信息组成一条回滚日志记录,插入到INDO LOG表中。

 ● 提交前,RM向TC注册分支事务,将其纳入XID对应全局事务的管辖:申请product表中,主键值等于1的记录的全局锁。

     6.本地事务提交:业务数据的更新和前面步骤中生成的UNDO LOG -并提交。

● TM向TC发起针对XID的全局提交或回滚决议。将本地事务提交的结果上报给TC

二阶段-提交 
●TC调度XID下管辖的全部分支事务完成提交或回滚请求。
        1.收到TC的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给TC。
        2.异步任务阶段的分支:提交请求将异步和批量地删除相应UNDO LOG记录。 
 因为“业务SQL"在-阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可

二阶段-回滚
●TC 调度XID下管辖的全部分支事务完成提交或回滚请求。
1.收到TC的分支回滚请求,开启- -个本地事务,执行如下操作。
2.通过XID和Branch ID查找到相应的UNDO LOG记录。
3.数据校验:拿UNDO LOG中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改(出现脏写), 转人工处理
(Seata因为无法感知这个脏写如何发生,此时只能打印日志和触发异常通知,告知用户需要人工介入)
4.如果没有脏写就简单了:根据UNDO LOG中的前镜像和业务SQL的相关信息生成并执行回滚的语句:
5.提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果),上报给 TC。 
回滚一阶段已经执行的“业务SQL" ,还原业务数据。回滚方式便是用"before image"还原业务数据;但在还原前要首先要校验脏写,对比”数据库当前业务数据”和"after image"如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
用法特别简单,服务端配置要配置仔细。客户端引入依赖包加上@globalTransactional注解即可。
但是在高并发场景下并不适用,因为seata有大量的锁,影响性能。转账的场景比较适用。

常见实现接口的幂等性     

  • 唯一id。每次操作,都根据操作和内容生成唯一 的id,在执行之前先判断id是否存在,如果不存在则执行后续操作,并且保存到数据库或者redis等。     
  • 服务端提供发送token的接口,业务调用接口前先获取token,然后调用业务接口请求时,把token携带过去,务 器判断token是否存在redis中,存在表示第一次请求, 可以继续执行业务,执行业务完成后,最后需要把 redis中的token删除     
  • 建去重表。将业务中有唯一 标识的字段保存到去重表,如果表中存在,则表示已经处理过了   
  • 版本控制。类似乐观锁增加版本号,当版本号符合时,才能更新数据     
  • 状态控制。例如订单有状态已支付未支付支付中支付失败,当处于未支付的时候才允许修改为支付中等

Dubbo的整体架构设计及分层     

五个角色:     

  • 注册中心registry:服务注册与发现     
  • 服务提供者provider:暴露服务   
  • 服务消费者consumer:调用远程服务     
  • 监控中心monitor:统计服务的调用次数和调用时间     
  • 容器container:服务允许容器 (启动的时候加载及运行provider)

调用流程:  

  1. container容器负责启动、 加载、运行provider
  2. provider在启动时,向regisitry中心注册自 己提供的服务  (以接口的维度提供服务)   
  3. consumer在启动时,向regisitry中心订阅自己所需的服务   
  4. regisitry返回服务提供者列表给consumer,如果有变更,registry将基于长连接推送变更数据给consumer   (regisitry与提供方维护的长连接主要是心跳机制,发现有服务长时间没有心跳则会删除,与消费者也是长连接维护,主要是发送服务端的列表,客户端调用服务端是基于负载均衡算法的)
  5. consumer调用provider服务,基于负载均衡算法进行调用   
  6. consumer调用provider的统计, 基于短链接定时每分钟- -次统计到monitor

分层:     

  • 接口服务层( Service)应用层 :面向开发者,业务代码、接口、实现等     
  • 配置层( Config) :对外配置接口,以ServiceConfig和ReferenceConfig为中心,一般用zk来实现 
  • 服务代理层( Proxy) :对生产者和消费者、dubbo都会产生一个代理类封装调用细节, 业务层对远程调用无感     
  • 服务注册层( Registry) : 封装服务地址的注册和发现,以服务URL为中心    也可用zk 
  • 路由层( Cluster) :封装多个提供者的路由和负载均衡,并桥接注册中心     
  • 监控层( Monitor) : RPC 调用次数和调用时间监控
  • 远程调用层( Protocal) :封装RPC调用     
  • 信息交换层( Exchange) :封装请求响应模式, 同步转异步     
  • 网络传输层( Transport) :抽象mina 和netty 为统-接口,统一网络传输接口     
  • 数据序列化层( Serialize) :数据传输的序列化和反序列化

Dubbo的底层网络通信:是基于Netty(主从线程组)网络架框架,而Netty是对NIO的封装,多路复用

SpringCloud 和 Dubbo对比     

  1. Dubbe, RPC的性能比HTTP的性能更好,并发能力更强,经过深度优化的RPC服务框架,性能和并发能力更好     
  2. Spring Cloud走HTTP协议,性能相比Dubbo的RPC性能差一些;     
  3. Spring Cloud主打的是微服务架构全家桶,组件齐全,功能齐全,Dubbo最初定位的是一个RPC框架, 很多的组件需要自己再做整合     
  4. 融合, Spring Cloud Alibaba,阿里技术将会融入Spring Cloud里面去,最终汇集 
  5. 注册中心: Spring Cloud使用的eureka,dubbo推荐使用zookeeper
  6.  模型定义: dubbo 将一个接口定义为-个服务,SpringCloud 则是将一个应用定义为一个服务
  7.  SpringCloud是一个生态, 而Dubbo是 SpringCloud生态中关于服务调用一种解决方案(服务治理) 

RPC服务性能和Http服务性能对比

序列化方式:
RPC服务序列化是针对二进制协议(0/1)来做序列化和反序列化,所以性能高。
而Http服务是基于文本的序列化和反序列化,需要读一行一行的文本(比如json格式的),进行序列化和反序列化,所以性能低。
报文长度:
RPC服务是自定义的传输协议,传输的报文都是干货。
而Http服务里面包括很多没用的报文内容,比如Http Header里面的accept,referer等等
连接的复用:
RPC服务是基于TCP/IP协议的,是长连接。
而Http服务大都是短连接,虽然Http1.1支持长连接,但是这个也是要取决于服务端是否支持长连接,不太可控。
结论
在追求性能的场合,用RPC服务(基于TCP/IP传输协议)
在追求兼容性多语言的场合,用REST服务

负载均衡算法、类型     

1.轮询法     

将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器, 而不关心服务器实际的连接数和当前的系统负载。     

2、随机法     

通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。     

3、源地址哈希法     

源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同- IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一-台后端服务器进行访问。     

4、加权轮询法     

不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载, 加权轮询能很好地处理这一问题, 并将请求顺序且按照权重分配到后端。     

5、加权随机法 

与加权轮询法一样, 加权随机法也根据后端机器的配置,系统的负载分配不同的权車。不同的是,它是按照权重随     机请求后端服务器,而非顺序。   

 6.最小连接数法     

最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务     器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。.     

类型:     DNS(域名解析)方式实现负载均衡   解析的时间损耗低,效率高

硬件负载均衡: F5 和A10 

软件负载均衡:     

Nginx、HAproxy、 LVS。 其中的区别:     

  • Nginx:七层负载均衡,支持HTTP. E-mail协议,同时也支持4负载均衡;   
  • HAproxy: 支持七层规则的,性能也很不错。OpenStack默认使用的负载均衡软件就是HAproxy;     

七层(在应用层做负载均衡根据URL的规则进行路由到service层,要建立连接客户端和服务端都要维护一个长连接导致性能低一些)

  • LVS:运行在内核态,性能是软件负载均衡中最高的,严格来说工作在三层(传输层做负载均衡,直接更改ip,效率也比较高),所以更通用一些,适用各种应用服务。

分布式架构下的session共享问题

目标:完成用户的状态认证   

单机版: session中, 服务器自动ioloiesesiondn--abced)     

集群版或者分布式:     有状态的方式、无状态的方式: .   

有状态     服务器依然要存储用户的信息,存储有效凭证     生成cookie(user. tokenuuid),从而来帮助我们确定存储在redis中的用户凭证信息。此处uuid等向于之前的sessionld.但是有状态的方式需要耗费一定的内存空间来存储用户信息服务端只需要给客户端一个凭证信息, 比如uuid即可

无状态     服务器不需要保存用户的状态信息     但是需要耗费一定的CPU资源来计算。     基于一种算法的方式JWT的方式token(用户本身的信息+时效信息) 依赖于解析的结果,如果发生异常。说明凭证已经时效,如果没有异常,说明状态正常

方式:

1.采用无状态服务,抛弃session(比如采用JWT json web token 生成凭证:基于算法生成令牌;验证凭证:基于算法核对令牌,令牌被篡改或过期都会抛异常) 

2.存入cookie (cookie是存在客户端(浏览器 )的,有安全风险)     

3、服务器之间进行Session同步,这样可以保证每个服务器上都有全部的Session信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败而且占资源;     

4、IP 绑定策略     使用Nginx (或其他复 杂均衡软硬件)中的IP绑定策略,同一个IP只能在指定的同一个机器访问,但是这样做失去了负载均衡的意义。当挂掉一台服务器的时候,会影响一批用户的使用,风险很大;     

5、使用Redis存储(使用广泛)     把Session放到Redis中存储,虽然架构上变得复杂,并且需要多访问一次Redis ,但是这种方案带来的好处也是很大的:     

  • 实现了Session共享;   
  • 可以水平扩展(增加Redis服务器) ; .   
  • 服务器重启Session不失(不过也要注意Session在Redis中的刷新/失效机制) ;  
  • 不仅可以跨服务器Session共享,甚至可以跨平台(例如网页端和APP端)。 

分布式系统中常用的缓存方案有哪些     

  • 客户端缓存:页面和浏览器缓存,APP缓存,H5缓存, localStorage 和sessionStorage   
  • CDN缓存:内容存储:数据的缓存,也可用来内容分发:负载均衡   
  • nginx缓存:静态资源   
  • 服务端缓存:本地續存,外部缓存     
  • 数据库缓存:持久层缓存(mybatis, hibernate多级缓存) , mysq|查询缓存   
  • 操作系统缓存: Page Cache. Buffer Cache 

定时任务实现原理     

优先队列:基于小顶堆实现(类似树结构,时间最短的是根节点),每次新增任务需要进行堆化,取任务时取堆顶元索、调整堆架构,时间复杂度是O(logN)     

时间轮算法:是一个环形队列,按照时间的单位区分,每个时间单位里面是一个链表、 用来存储定时任务,像时钟一样轮询环形队列,取出链表中的任务执行,如果超出了环形队列的时间粒度、可以使用多级时间轮,即使用不同维度的时间单位,就跟时钟或者水表一样,这一层的走了一圈,下一层的才走了一格,时间复杂度为O(1) 

Nacos注册中心原理

服务注册与发现,适配Spring Cloud服务注册与发现标准,默认集成了Ribbon的支持(@LoadBalanced负载均衡注解)

  • 服务注册:当服务启动通过Rest请求的方式向Nacos Server注册自己的服务。将服务端的ip端口等信息封装为instance放到一个阻塞队列BlockQueue中,后台有一个线程进行消费写到注册表中。异步注册的原因是为了支持高并发,使服务系统的启动不受注册时间的影响。注册表结构:Map<namespace,Map<group::serviceName,Service>>,Service中又有Set。高可扩展。namespace可以区分开发/测试等不同环境,group可以对微服务分组(如按地区分组等)
    注册中心的读写分离是通过CopyOnWrite(写时复制,复制的粒度控制在最小,修改时不是复制所有)实现的,解决读写并发冲突,在更新服务注册列表时先复制出来一份,这个操作是单线程的,每个服务等上一个服务对副本写完并替换完成之后再去复制出一份副本,进行修改的,每个服务注册都是在前一个服务注册完之后再注册的。客户端正常读取当前的列表,服务端对副本进行更新,更新之后替换原来的。如果有对注册表更新,可以只copy想更新的service部分,不用copy整个注册表,这也是双层map的好处之一。
  • 服务心跳: Nacose Client会维护一个定时心跳持(就是一个http服务接口的调用,每次更新实例信息,心跳时间等)续通知Nacos Server(也是一个http服务接口的调用) ,默认5s一次,如果nacosClient超过15秒没有接收心跳,会将服务健康状态设置false (拉取的时候会忽略),如果nacos Client超过了30秒没有接收心跳剔除服务。客户端也有一个定时任务,从注册中心拉取最新的服务列表替代本地的服务列表。
  • 服务发现: Nacose Client会有一个定时任务,实时去Nacos Server拉取健康服务。
  • 服务停止: Nacose Client会主动通过Rest请求Nacos Server发送一个注销的请求 

服务的调用还要引入openfeign,开启@EnableFeignClients,接口加@GeignClent(value=“调用的远端服务名称 ”),方法上@XXXMapping(value=“远端接口服务地址”)。feign的底层通过代理实现,将服务名称及接口URL拼接好并发起请求,feign的底层也是ribbon,不用feign和ribbon的话用dubbo也可以。

脑裂:一般存在于cp架构的集群架构,由于网络分区导致主节点出问题,其他分区又选出了新的主节点,集群中出现了多个leader,客户端的写不一致,导致数据不一致。

Nacos配置中心原理

Nacos服务端创建了相关的配置项后,客户端就可以进行监听了。
客户端是通过一个定时任务来检查自己监听的配置项的数据的,一旦服务端的数据发生变化时(双方的md5的值不相同),客户端将会获取到最新的数据,并将最新的数据保存在一个CacheData对象中,然后会重新计算CacheData的md5属性的值,此时就会对该CacheData所绑定的Listener触发receiveConfgInfo (接收配置信息)回调。

Sentinel:分布式系统的流量防卫兵

降级:引入Sentinel包,目标接口@FeignClient(value=“远端服务名称”,fallback=降级ServiceFallback.class)降级类中实现降级逻辑的处理,如处理失败的日志记录等
限流:运行sentinel的服务端jar包,做好配置。sentinel服务端会发现客户端应用程序,对于发起请求的URL,sentinel会通过客户端的拦截器进行拦截并收集。sentinel页面可以配置流控等等。
熔断:某接口远端调用无法继续的时候,后续就不再调用远端服务,改为本地调用进行降级。在sentinel页面配置降级规则

sentinel限流的底层是基于滑动窗口计数器实现的

nacos注册中心简单使用

服务网关可以做什么 
 

微服务架构排错

定位服务节点 、查看日志等
工具:springboot admin监控页面定位服务节点、Skywalking链路追踪慢请求慢响应

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值