1.分布式事务

2. 分布式锁

Java 原生 API 虽然有并发锁,但并没有提供分布式锁的能力,所以针对分布式场景中的锁需要解决的方案。

分布式锁的解决方案大致有以下几种:

基于数据库实现基于缓存(redis,memcached 等)实现基于 Zookeeper 实现

2.1. 基于数据库实现分布式锁

实现

1. 创建表

CREATETABLE`methodLock`(`id`int(11)NOTNULLAUTO_INCREMENTCOMMENT'主键',`method_name`varchar(64)NOTNULLDEFAULT''COMMENT'锁定的方法名',`desc`varchar(1024)NOTNULLDEFAULT'备注信息',`update_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'保存数据时间,自动生成',PRIMARYKEY(`id`),UNIQUEKEY`uidx_method_name`(`method_name`)USINGBTREE)ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='锁定中的方法';

2. 获取锁

想要锁住某个方法时,执行以下 SQL:

insertintomethodLock(method_name,desc)values(‘method_name’,‘desc’)

因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

成功插入则获取锁。

3. 释放锁

当方法执行完毕之后,想要释放锁的话,需要执行以下 Sql:

deletefrommethodLockwheremethod_name='method_name'

问题这把锁强依赖数据库的可用性。如果数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。这把锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

解决办法单点问题可以用多数据库实例,同时塞 N 个表,N/2+1 个成功就任务锁定成功写一个定时任务,隔一段时间清除一次过期的数据。写一个 while 循环,不断的重试插入,直到成功。在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

小结优点: 直接借助数据库,容易理解。缺点: 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。操作数据库需要一定的开销,性能问题需要考虑。

2.2. 基于 Redis 实现分布式锁

相比于用数据库来实现分布式锁,基于缓存实现的分布式锁的性能会更好一些。目前有很多成熟的分布式产品,包括 Redis、memcache、Tair 等。这里以 Redis 举例。

Redis 命令setnx - setnx key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;若 key 存在,则什么都不做,返回 0。expire - expire key timeout:为 key 设置一个超时时间,单位为 second,超过这个时间锁会自动释放,避免死锁。delete - delete key:删除 key

实现

单点实现步骤:

获取锁的使用,使用 setnx 加锁,锁的 value 值为一个随机生成的 UUID,再使用 expire 设置一个过期值。获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。

问题单点问题。如果单机 redis 挂掉了,那么程序会跟着出错。如果转移使用 slave 节点,复制不是同步复制,会出现多个程序获取锁的情况

小结

可以考虑使用redisson 的解决方案。

2.3. 基于 ZooKeeper 实现分布式锁

实现

这也是 ZooKeeper 客户端 curator 的分布式锁实现。

创建一个目录 mylock;线程 A 想获取锁就在 mylock 目录下创建临时顺序节点;获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

小结

ZooKeeper 版本的分布式锁问题相对比较来说少。

锁的占用时间限制:redis 就有占用时间限制,而 ZooKeeper 则没有,最主要的原因是 redis 目前没有办法知道已经获取锁的客户端的状态,是已经挂了呢还是正在执行耗时较长的业务逻辑。而 ZooKeeper 通过临时节点就能清晰知道,如果临时节点存在说明还在执行业务逻辑,如果临时节点不存在说明已经执行完毕释放锁或者是挂了。由此看来 redis 如果能像 ZooKeeper 一样添加一些与客户端绑定的临时键,也是一大好事。是否单点故障:redis 本身有很多中玩法,如客户端一致性 hash,服务器端 sentinel 方案或者 cluster 方案,很难做到一种分布式锁方式能应对所有这些方案。而 ZooKeeper 只有一种玩法,多台机器的节点数据是一致的,没有 redis 的那么多的麻烦因素要考虑。

总体上来说 ZooKeeper 实现分布式锁更加的简单,可靠性更高。但 ZooKeeper 因为需要频繁的创建和删除节点,性能上不如 Redis 方式。

3. 分布式 Session

在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。

分布式 Session 的几种实现策略:

粘性 session应用服务器间的 session 复制共享基于 cache DB 缓存的 session 共享

3.1. Sticky Sessions

需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。

缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。

3.2. Session Replication

在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。

缺点:占用过多内存;同步过程占用网络带宽以及服务器处理器时间。

3.3. Session Server

使用一个单独的服务器存储 Session 数据,可以存在 MySQL 数据库上,也可以存在 Redis 或者 Memcached 这种内存型数据库。

缺点:需要去实现存取 Session 的代码。

4. 分布式存储

通常有两种解决方案:

数据分布:就是把数据分块存在不同的服务器上(分库分表)。数据复制:让所有的服务器都有相同的数据,提供相当的服务。

5. 分布式缓存

使用缓存的好处:

提升数据读取速度提升系统扩展能力,通过扩展缓存,提升系统承载能力降低存储成本,Cache+DB 的方式可以承担原有需要多台 DB 才能承担的请求量,节省机器成本

根据业务场景,通常缓存有以下几种使用方式

懒汉式(读时触发):写入 DB 后, 然后把相关的数据也写入 Cache饥饿式(写时触发):先查询 DB 里的数据, 然后把相关的数据写入 Cache定期刷新:适合周期性的跑数据的任务,或者列表型的数据,而且不要求绝对实时性

缓存分类:

应用内缓存:如:EHCache分布式缓存:如:Memached、Redis

6. 分布式计算

7. 负载均衡

7.1. 算法

轮询(Round Robin)

轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。

该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。

加权轮询(Weighted Round Robbin)

加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。

最少连接(least Connections)

由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。

最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。

加权最少连接(Weighted Least Connection)

在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。

随机算法(Random)

把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。

源地址哈希法 (IP Hash)

源地址哈希通过对客户端 IP 哈希计算得到的一个数值,用该数值对服务器数量进行取模运算,取模结果便是目标服务器的序号。

优点:保证同一 IP 的客户端都会被 hash 到同一台服务器上。缺点:不利于集群扩展,后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。

7.2. 实现

HTTP 重定向

HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。

缺点:

用户访问的延迟会增加;如果负载均衡器宕机,就无法访问该站点。

DNS 重定向

使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。

缺点:

DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。

修改 MAC 地址

使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。

修改 IP 地址

在网络层修改请求的目的 IP 地址。

代理自动配置

正向代理与反向代理的区别:

正向代理:发生在客户端,是由用户主动发起的。比如外网,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。反向代理:发生在服务器端,用户不知道代理的存在。

PAC 服务器是用来判断一个请求是否要经过代理。