一个系统业务量很小的时候所有的代码都放在一个项目中就好了,然后把这个项目部署在一台服务器上就可以了。整个项目的所有服务都由这台服务器提供。这就是单机结构。但是单机结构的缺点非常明显,当业务量增加到一定程度的时候,单机的硬件资源无法满足业务要求(单机容量问题),此时出现了集群模式。
单机处理达到瓶颈的时候,将单机复制几份,这样就构成了一个集群。集群中每台服务器就叫做这个集群的一个节点,所有节点构成一个集群。每个节点都提供相同的服务,这样系统的处理能力就相当于提升了好几倍。
但这里需要考虑的一个问题是,用户的请求由哪个服务器来处理呐?
—让此时负载较小的节点来处理,这样使得每个节点的压力都比较平均。
因此,我们需要在所有节点之前增加一个负载均衡服务器,用户所有的请求都交给他,然后负载均衡器根据当前所有节点的负载情况,决定将这个请求交给哪个节点进行处理。
但是,当业务发展到一定程度的时候,发现无论怎么增加节点,整个集群的性能提升效果不明显了,这个时候,我们需要使用分布式。
这里,发现从单机结构到集群结构,代码基本不需要做任何修改,仅仅是多部署几台服务器,每台服务器上运行相同的代码就行了。
分布式,就是将一个完整的系统按照业务功能,拆分成一个个的独立的子系统,在分布式结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在web容器中,他们通过RPC方式通信。
比如秒杀系统项目中,我们需要按照功能模块拆分成多个独立的服务:用户服务,商品服务,订单服务,库存服务等。这一个个服务都是一个独立的项目,可以独立运行。如果服务之间有依赖关系,可以通过RPC方式调用。降低了系统之间的耦合度,可以独立开发,独立部署,独立测试。
分布式就是将后台工作分布在多个服务器上,多个服务器协同完成工作
数据不一致问题
秒杀系统,库存只有一份,所有人会在集中的时间读和写这些数据,多个人读一个数据。(读写冲突)
通信异常
分布式系统将原有的单机通信,变成各个节点依赖网络进行通信,由于网络本身的不可靠性,都会导致分布式系统无法顺利完成一次网络通信。即使完成了一次通信,也需要考虑时间上的延迟。
订单服务插入之后要调用库存服务更新库存,库存数据不是特别敏感,不需要强一致性,可以通过最终一致性方案实现。
订单服务和库存服务,每个服务维护了自己的数据库,在交易系统的业务逻辑中,一个商品在下单前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。正常情况下,两边数据库各自更新成功,两边数据维持着一致性。但是在非正常情况下,有可能库存扣减完了,随后订单记录却因为某些原因插入失败。这个时候两边的数据就失去了应有的一致性。
这个时候必须要保证数据的一致性,但数据源的一致性依靠单机事务来保证,多数据源的一致性就要依靠分布式事务。
事务:单个逻辑单元执行的一组操作,要么全成功,要么全失败;事务的四大特性是:持久性,一致性,隔离性,原子性。
分布式事务:分布式事务用于在分布式系统中保证不同节点之间的数据一致性。分布式事务的实现有很多种,最具代表性的是两阶段提交(XA协议的一种)。
两阶段提交:
角色:事务协调者,事务参与者
第一阶段:
在XA分布式的第一阶段,作为事务协调者的节点会向所有参与者节点发送prepare请求;
在接到prepare请求之后,每一个参与者节点会各自执行与事务有关的数据更新,写入undo log 和redo log。如果参与者执行成功,暂时不提交事务,而是向事务协调节点返回“完成”消息;
当事务协调者接到了所有参与者的返回消息,整个分布式事务将会进入第二阶段。
第二阶段:
在XA分布式事务的第二阶段,如果事务协调点在之前所收到的都是正向返回,那么他将会向所有事务参与者发出commit请求;
接到commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“完成”消息;
当事务协调者收到所有事务参与者的“完成”反馈,整个分布式事务完成。
以上所描述的是XA两阶段提交的正向流程,接下来我们看一下失败情况的处理流程
在XA的第一阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。
于是在第二阶段,事务协调者会向所有的事务参与者发送Abort请求。接收到Abort请求之后,各个事务参与者节点需要在本地进行事务的回滚操作,回滚操作依照undo log 来进行。
XA两阶段提交虽然解决了分布式事务的一致性问题,但也存在着不足
①性能问题:
XA协议遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这个过程有着非常明显的性能问题
②协调者单点故障问题:
事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或者回滚通知,参与者会一直处于中间状态无法完成事务。
③丢失消息导致的不一致问题:
在XA协议的第二阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另外一部分事务参与者没收到提交消息,这届导致了节点之间数据的不一致。
如何避免两阶段提交的种种问题呐?
XA三阶段提交在两阶段的基础上增加了CanCommit阶段,并且引入了超时机制。一旦事务参与者迟迟没有收到事务协调者的commit请求,会自动进行本地commit。这样有效解决了协调者单点故障的问题。
性能问题和不一致问题仍然没有根本解决
MQ事务:利用消息中间件来异步完成事务的后一半更新,实现系统的最终一致性。
这个方式避免了向XA协议那样的性能问题
TCC事务:TCC事务是try,commit,cancel三种指令的缩写,它的逻辑模式类似于XA两阶段提交,但是实现方式是在代码层面来人为实现。
如果在一个分布式系统中,我们从数据库中读取一个数据,然后修改保存,这种情况很容易遇到并发问题。因为读取和更新保存不是一个原子操作,在并发时就会导致数据的不正确。比如电商的秒杀活动,库存数量的更新就会遇到。
分布式式锁目的:为了保证多台服务器在执行某一段代码时保证只有一台服务器执行。
为了保证分布式锁的可用性,至少要保证锁的实现要同时满足一下几点:
互斥性:在任何时刻,保证只有一个客户端持有锁
不能出现死锁:如果在一个客户端持有锁的期间,这个客户端崩溃了,也要保证后续的其他客户端可以上锁。
保证上锁和解锁都是同一个客户端
一般来说,实现分布式锁的方式有以下几种:
①使用MySQL,基于唯一索引。
②使用ZooKeeper,基于临时有序节点
③使用Redis,基于setnx命令。
Redis实现分布式锁主要利用Redis的setnx命令。setnx是 SET if not exists的缩写。
加锁:使用setnx key value命令,如果key不存在,设置value(加锁成功),如果已经存在lock(也就是客户端持有锁了),则设置失败(加锁失败)
setnx lock value1
解锁:使用del命令,通过删除键值释放锁。释放锁之后,其他客户端可以通过setnx命令进行加锁。
del lock
private static Jedis jedis = new Jedis("127.0.0.1");
private static final Long SUCCESS = 1L;
//加锁
public boolean tryLock(String key, String requestId) {
//使用setnx命令
//不存在则保存返回1,加锁成功;如果已经存在则返回0,加锁失败。
return SUCCESS.equals(jedis.setnx(Key, requestId));
}
//删除key的lua脚本,先比较requestId是否相等,相等则删除
private static final String DEL_SCRIPT = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//解锁
public boolean unlock(String key, String requsetId) {
//删除成功表示解锁成功
Long result = (long) jedis.eval(DEL_SCRIPT, Collections.singletonList(key),Collections.singletonList(requestId));
return SUCCESS.equals(result);
}