单机、集群和分布式相关概念理解

单机、集群和分布式相关概念理解

单机

一个系统业务量很小的时候所有的代码都放在一个项目中就好了,然后把这个项目部署在一台服务器上就可以了。整个项目的所有服务都由这台服务器提供。这就是单机结构。但是单机结构的缺点非常明显,当业务量增加到一定程度的时候,单机的硬件资源无法满足业务要求(单机容量问题),此时出现了集群模式。

集群

单机处理达到瓶颈的时候,将单机复制几份,这样就构成了一个集群。集群中每台服务器就叫做这个集群的一个节点,所有节点构成一个集群。每个节点都提供相同的服务,这样系统的处理能力就相当于提升了好几倍。

但这里需要考虑的一个问题是,用户的请求由哪个服务器来处理呐?

—让此时负载较小的节点来处理,这样使得每个节点的压力都比较平均。

因此,我们需要在所有节点之前增加一个负载均衡服务器,用户所有的请求都交给他,然后负载均衡器根据当前所有节点的负载情况,决定将这个请求交给哪个节点进行处理。

但是,当业务发展到一定程度的时候,发现无论怎么增加节点,整个集群的性能提升效果不明显了,这个时候,我们需要使用分布式。

这里,发现从单机结构到集群结构,代码基本不需要做任何修改,仅仅是多部署几台服务器,每台服务器上运行相同的代码就行了。

分布式

分布式,就是将一个完整的系统按照业务功能,拆分成一个个的独立的子系统,在分布式结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在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);
}

你可能感兴趣的:(Java学习,分布式,数据库,redis,java)