--- title: 大型系统核心技术 date: 2018-07-09 00:00:00 order: 08 categories: - 设计 - 架构 - 综合 tags: - 架构 - 分布式 permalink: /pages/53415678/ --- # 大型系统核心技术 > 大型系统的设计目标就是为了快速、高效、稳定的处理海量的数据以及高并发的请求。 > > 单机服务受限于硬件,客观存在着资源瓶颈,难以应对不断增长的数据量和请求量,为了打破瓶颈,大型系统基本上都被设计为分布式系统。 > > 分布式系统由于其面临的共性问题,在很多场景下的解决方案往往也存在着共性。因此,我们会发现,很多优秀的大型系统在设计方案上存在着很多的共同点。 > > 本文主要讨论应对分布式系统共性问题的解决方案,这既可以加深对分布式系统运作原理的理解,也可以作为设计大型分布式系统时的借鉴。 ## 分布式事务 ## 分布式锁 Java 原生 API 虽然有并发锁,但并没有提供分布式锁的能力,所以针对分布式场景中的锁需要解决的方案。 分布式锁的解决方案大致有以下几种: - 基于数据库实现 - 基于缓存(redis,memcached 等)实现 - 基于 Zookeeper 实现 ### 基于数据库实现分布式锁 #### 实现 ##### 创建表 ```sql CREATE TABLE `methodLock` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名', `desc` varchar(1024) NOT NULL DEFAULT '备注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法'; ``` ##### 获取锁 想要锁住某个方法时,执行以下 SQL: ```sql insert into methodLock(method_name,desc) values (‘method_name’,‘desc’) ``` 因为我们对 `method_name` 做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。 成功插入则获取锁。 ##### 释放锁 当方法执行完毕之后,想要释放锁的话,需要执行以下 Sql: ```sql delete from methodLock where method_name ='method_name' ``` #### 问题 1. 这把锁强依赖数据库的可用性。如果数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。 2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。 3. 这把锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。 4. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。 #### 解决办法 1. 单点问题可以用多数据库实例,同时塞 N 个表,N/2+1 个成功就任务锁定成功 2. 写一个定时任务,隔一段时间清除一次过期的数据。 3. 写一个 while 循环,不断的重试插入,直到成功。 4. 在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。 #### 小结 - 优点: 直接借助数据库,容易理解。 - 缺点: 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。操作数据库需要一定的开销,性能问题需要考虑。 ### 基于 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 #### 实现 单点实现步骤: 1. 获取锁的使用,使用 setnx 加锁,锁的 value 值为一个随机生成的 UUID,再使用 expire 设置一个过期值。 2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。 3. 释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。 #### 问题 - 单点问题。如果单机 redis 挂掉了,那么程序会跟着出错。 - 如果转移使用 slave 节点,复制不是同步复制,会出现多个程序获取锁的情况 #### 小结 可以考虑使用 [redisson 的解决方案](https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8)。 ### 基于 ZooKeeper 实现分布式锁 #### 实现 这也是 ZooKeeper 客户端 curator 的分布式锁实现。 1. 创建一个目录 mylock; 2. 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点; 3. 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; 4. 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; 5. 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 #### 小结 ZooKeeper 版本的分布式锁问题相对比较来说少。 - 锁的占用时间限制:redis 就有占用时间限制,而 ZooKeeper 则没有,最主要的原因是 redis 目前没有办法知道已经获取锁的客户端的状态,是已经挂了呢还是正在执行耗时较长的业务逻辑。而 ZooKeeper 通过临时节点就能清晰知道,如果临时节点存在说明还在执行业务逻辑,如果临时节点不存在说明已经执行完毕释放锁或者是挂了。由此看来 redis 如果能像 ZooKeeper 一样添加一些与客户端绑定的临时键,也是一大好事。 - 是否单点故障:redis 本身有很多中玩法,如客户端一致性 hash,服务器端 sentinel 方案或者 cluster 方案,很难做到一种分布式锁方式能应对所有这些方案。而 ZooKeeper 只有一种玩法,多台机器的节点数据是一致的,没有 redis 的那么多的麻烦因素要考虑。 总体上来说 ZooKeeper 实现分布式锁更加的简单,可靠性更高。但 ZooKeeper 因为需要频繁的创建和删除节点,性能上不如 Redis 方式。 ## 分布式 Session 在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。 分布式 Session 的几种实现策略: 1. 粘性 session 2. 应用服务器间的 session 复制共享 3. 基于 cache DB 缓存的 session 共享 ### Sticky Sessions 需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。 缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。