当前位置: 首页 > news >正文

网站建设私活施工企业三大体系认证

网站建设私活,施工企业三大体系认证,ios软件开发教程,常熟东南开发区人才网前言 随着互联网信息技术的飞速发展#xff0c;数据量不断增大#xff0c;业务逻辑也日趋复杂#xff0c;对系统的高并发访问、海量数据处理的场景也越来越多。 如何用较低成本实现系统的高可用、易伸缩、可扩展等目标就显得越发重要。为了解决这一系列问题#xff0c;系…前言 随着互联网信息技术的飞速发展数据量不断增大业务逻辑也日趋复杂对系统的高并发访问、海量数据处理的场景也越来越多。 如何用较低成本实现系统的高可用、易伸缩、可扩展等目标就显得越发重要。为了解决这一系列问题系统架构也在不断演进。传统的集中式系统已经逐渐无法满足要求分布式系统被使用在更多的场景中。 分布式系统由独立的服务器通过网络松散耦合组成。在这个系统中每个服务器都是一台独立的主机服务器之间通过内部网络连接。分布式系统有以下几个特点 可扩展性可通过横向水平扩展提高系统的性能和吞吐量。 高可靠性高容错即使系统中一台或几台故障系统仍可提供服务。 高并发性各机器并行独立处理和计算。 廉价高效多台小型机而非单台高性能机。 然而在分布式系统中其环境的复杂度、网络的不确定性会造成诸如时钟不一致、“拜占庭将军问题”Byzantine failure等。存在于集中式系统中的机器宕机、消息丢失等问题也会在分布式环境中变得更加复杂。 基于分布式系统的这些特征有两种问题逐渐成为了分布式环境中需要重点关注和解决的典型问题 互斥性问题。 幂等性问题。 今天我们就针对这两个问题来进行分析。 先看两个常见的例子 例1某服务记录关键数据X当前值为100。A请求需要将X增加200同时B请求需要将X减100。 在理想的情况下A先读取到X100然后X增加200最后写入X300。B请求接着从读取X300减少100最后写入X200。 然而在真实情况下如果不做任何处理则可能会出现A和B同时读取到X100A写入之前B读取到XB比A先写入等等情况。 例2某服务提供一组任务A请求随机从任务组中获取一个任务B请求随机从任务组中获取一个任务。 在理想的情况下A从任务组中挑选一个任务任务组删除该任务B从剩下的的任务中再挑一个任务组删除该任务。 同样的在真实情况下如果不做任何处理可能会出现A和B挑中了同一个任务的情况。 以上的两个例子都存在操作互斥性的问题。互斥性问题用通俗的话来讲就是对共享资源的抢占问题。如果不同的请求对同一个或者同一组资源读取并修改时无法保证按序执行无法保证一个操作的原子性那么就很有可能会出现预期外的情况。因此操作的互斥性问题也可以理解为一个需要保证时序性、原子性的问题。 在传统的基于数据库的架构中对于数据的抢占问题往往是通过数据库事务ACID来保证的。在分布式环境中出于对性能以及一致性敏感度的要求使得分布式锁成为了一种比较常见而高效的解决方案。 事实上操作互斥性问题也并非分布式环境所独有在传统的多线程、多进程情况下已经有了很好的解决方案。因此在研究分布式锁之前我们先来分析下这两种情况的解决方案以期能够对分布式锁的解决方案提供一些实现思路。 多线程环境解决方案及原理 解决方案 《Thinking in Java》书中写到基本上所有的并发模式在解决线程冲突问题的时候都是采用序列化访问共享资源的方案。在多线程环境中线程之间因为公用一些存储空间冲突问题时有发生。解决冲突问题最普遍的方式就是用互斥锁把该资源或对该资源的操作保护起来。 Java JDK中提供了两种互斥锁Lock和synchronized。不同的线程之间对同一资源进行抢占该资源通常表现为某个类的普通成员变量。因此利用ReentrantLock或者synchronized将共享的变量及其操作锁住即可基本解决资源抢占的问题。 下面来简单聊一聊两者的实现原理。 原理 ReentrantLock ReentrantLock主要利用CASCLH队列来实现。它支持公平锁和非公平锁两者的实现类似。 CASCompare and Swap比较并交换。CAS有3个操作数内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时将内存值V修改为B否则什么都不做。该操作是一个原子操作被广泛的应用在Java的底层实现中。在Java中CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。 CLH队列带头结点的双向非循环链表(如下图所示) ReentrantLock的基本实现可以概括为先通过CAS尝试获取锁。如果此时已经有线程占据了锁那就加入CLH队列并且被挂起。当锁被释放之后排在CLH队列队首的线程会被唤醒然后CAS再次尝试获取锁。在这个时候如果 非公平锁如果同时还有另一个线程进来尝试获取那么有可能会让这个线程抢先获取 公平锁如果同时还有另一个线程进来尝试获取当它发现自己不是在队首的话就会排到队尾由队首的线程获取到锁。 下面分析下两个片段 final boolean nonfairTryAcquire(int acquires) {final Thread current Thread.currentThread();int c getState();if (c 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current getExclusiveOwnerThread()) {int nextc c acquires;if (nextc 0) // overflowthrow new Error(Maximum lock count exceeded);setState(nextc);return true;}return false; }在尝试获取锁的时候会先调用上面的方法。如果状态为0则表明此时无人占有锁。此时尝试进行set一旦成功则成功占有锁。如果状态不为0再判断是否是当前线程获取到锁。如果是的话将状态1因为此时就是当前线程所以不用CAS。这也就是可重入锁的实现原理。 final boolean acquireQueued(final Node node, int arg) {boolean failed true;try {boolean interrupted false;for (;;) {final Node p node.predecessor();if (p head tryAcquire(arg)) {setHead(node);p.next null; // help GCfailed false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) parkAndCheckInterrupt())interrupted true;}} finally {if (failed)cancelAcquire(node);} } private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted(); }该方法是在尝试获取锁失败加入CHL队尾之后如果发现前序节点是head则CAS再尝试获取一次。否则则会根据前序节点的状态判断是否需要阻塞。如果需要阻塞则调用LockSupport的park方法阻塞该线程。 synchronized 在Java语言中存在两种内建的synchronized语法synchronized语句、synchronized方法。 synchronized语句当源代码被编译成字节码的时候会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit字节码指令; synchronized方法在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1。这个在specification中没有明确说明。 monitorenterThe objectref must be of type reference. Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: - If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. - If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. - If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.每个对象都有一个锁也就是监视器monitor。当monitor被占有时就表示它被锁定。线程执行monitorenter指令时尝试获取对象所对应的monitor的所有权过程如下 如果monitor的进入数为0则该线程进入monitor然后将进入数设置为1该线程即为monitor的所有者; 如果线程已经拥有了该monitor只是重新进入则进入monitor的进入数加1; 如果其他线程已经占用了monitor则该线程进入阻塞状态直到monitor的进入数为0再重新尝试获取monitor的所有权。 monitorexitThe objectref must be of type reference. The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.执行monitorexit的线程必须是相应的monitor的所有者。 指令执行时monitor的进入数减1如果减1后进入数为0那线程退出monitor不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。 在JDK1.6及其之前的版本中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行这种切换的代价是非常昂贵的。然而在现实中的大部分情况下同步方法是运行在单线程环境无锁竞争环境。如果每次都调用Mutex Lock将严重的影响程序的性能。因此在JDK 1.6之后的版本中对锁的实现做了大量的优化这些优化在很大程度上减少或避免了Mutex Lock的使用。 多进程的解决方案 在多道程序系统中存在许多进程它们共享各种资源然而有很多资源一次只能供一个进程使用这便是临界资源。多进程中的临界资源大致上可以分为两类一类是物理上的真实资源如打印机一类是硬盘或内存中的共享数据如共享内存等。而进程内互斥访问临界资源的代码被称为临界区。 针对临界资源的互斥访问JVM层面的锁就已经失去效力了。在多进程的情况下主要还是利用操作系统层面的进程间通信原理来解决临界资源的抢占问题。比较常见的一种方法便是使用信号量Semaphores。 信号量在POSIX标准下有两种分别为有名信号量和无名信号量。无名信号量通常保存在共享内存中而有名信号量是与一个特定的文件名称相关联。信号量是一个整数变量有计数信号量和二值信号量两种。对信号量的操作主要是P操作wait和V操作signal。 P操作先检查信号量的大小若值大于零则将信号量减1同时进程获得共享资源的访问权限继续执行若小于或者等于零则该进程被阻塞后进入等待队列。 V操作该操作将信号量的值加1如果有进程阻塞着等待该信号量那么其中一个进程将被唤醒。 举个例子设信号量为1当一个进程A在进入临界区之前先进行P操作。发现值大于零那么就将信号量减为0进入临界区执行。此时若另一个进程B也要进去临界区进行P操作发现信号量等于0则会被阻塞。当进程A退出临界区时会进行V操作将信号量的值加1并唤醒阻塞的进程B。此时B就可以进入临界区了。 这种方式其实和多线程环境下的加解锁非常类似。因此用信号量处理临界资源抢占也可以简单地理解为对临界区进行加锁。 通过上面的一些了解我们可以概括出解决互斥性问题即资源抢占的基本方式为 对共享资源的操作前后进入退出临界区加解锁保证不同线程或进程可以互斥有序的操作资源。 加解锁方式有显式的加解锁如ReentrantLock或信号量也有隐式的加解锁如synchronized。那么在分布式环境中为了保证不同JVM不同主机间不会出现资源抢占那么同样只要对临界区加解锁就可以了。 然而在多线程和多进程中锁已经有比较完善的实现直接使用即可。但是在分布式环境下就需要我们自己来实现分布式锁。 分布式环境下的解决方案——分布式锁 首先我们来看看分布式锁的基本条件。 分布式锁条件 基本条件 再回顾下多线程和多进程环境下的锁可以发现锁的实现有很多共通之处它们都需要满足一些最基本的条件 1. 需要有存储锁的空间并且锁的空间是可以访问到的。 2. 锁需要被唯一标识。 3. 锁要有至少两种状态。 仔细分析这三个条件 存储空间 锁是一个抽象的概念锁的实现需要依存于一个可以存储锁的空间。在多线程中是内存在多进程中是内存或者磁盘。更重要的是这个空间是可以被访问到的。多线程中不同的线程都可以访问到堆中的成员变量在多进程中不同的进程可以访问到共享内存中的数据或者存储在磁盘中的文件。但是在分布式环境中不同的主机很难访问对方的内存或磁盘。这就需要一个都能访问到的外部空间来作为存储空间。 最普遍的外部存储空间就是数据库了事实上也确实有基于数据库做分布式锁行锁、version乐观锁如quartz集群架构中就有所使用。除此以外还有各式缓存如Redis、Tair、Memcached、Mongodb当然还有专门的分布式协调服务Zookeeper甚至是另一台主机。只要可以存储数据、锁在其中可以被多主机访问到那就可以作为分布式锁的存储空间。 唯一标识 不同的共享资源必然需要用不同的锁进行保护因此相应的锁必须有唯一的标识。在多线程环境中锁可以是一个对象那么对这个对象的引用便是这个唯一标识。多进程环境中信号量在共享内存中也是由引用来作为唯一的标识。但是如果不在内存中失去了对锁的引用如何唯一标识它呢上文提到的有名信号量便是用硬盘中的文件名作为唯一标识。因此在分布式环境中只要给这个锁设定一个名称并且保证这个名称是全局唯一的那么就可以作为唯一标识。 至少两种状态 为了给临界区加锁和解锁需要存储两种不同的状态。如ReentrantLock中的status0表示没有线程竞争大于0表示有线程竞争信号量大于0表示可以进入临界区小于等于0则表示需要被阻塞。因此只要在分布式环境中锁的状态有两种或以上如有锁、没锁存在、不存在等等均可以实现。 有了这三个条件基本就可以实现一个简单的分布式锁了。下面以数据库为例实现一个简单的分布式锁 数据库表字段为锁的ID唯一标识锁的状态0表示没有被锁1表示被锁。 伪代码为 lock mysql.get(id); while(lock.status 1) {sleep(100); } mysql.update(lock.status 1); doSomething(); mysql.update(lock.status 0);问题 以上的方式即可以实现一个粗糙的分布式锁但是这样的实现有没有什么问题呢 问题1锁状态判断原子性无法保证 从读取锁的状态到判断该状态是否为被锁需要经历两步操作。如果不能保证这两步的原子性就可能导致不止一个请求获取到了锁这显然是不行的。因此我们需要保证锁状态判断的原子性。 问题2网络断开或主机宕机锁状态无法清除 假设在主机已经获取到锁的情况下突然出现了网络断开或者主机宕机如果不做任何处理该锁将仍然处于被锁定的状态。那么之后所有的请求都无法再成功抢占到这个锁。因此我们需要在持有锁的主机宕机或者网络断开的时候及时的释放掉这把锁。 问题3无法保证释放的是自己上锁的那把锁 在解决了问题2的情况下再设想一下假设持有锁的主机A在临界区遇到网络抖动导致网络断开分布式锁及时的释放掉了这把锁。之后另一个主机B占有了这把锁但是此时主机A网络恢复退出临界区时解锁。由于都是同一把锁所以A就会将B的锁解开。此时如果有第三个主机尝试抢占这把锁也将会成功获得。因此我们需要在解锁时确定自己解的这个锁正是自己锁上的。 进阶条件 如果分布式锁的实现还能再解决上面的三个问题那么就可以算是一个相对完整的分布式锁了。然而在实际的系统环境中还会对分布式锁有更高级的要求。 1.可重入线程中的可重入指的是外层函数获得锁之后内层也可以获得锁ReentrantLock和synchronized都是可重入锁衍生到分布式环境中一般仍然指的是线程的可重入在绝大多数分布式环境中都要求分布式锁是可重入的。 2.惊群效应Herd Effect在分布式锁中惊群效应指的是在有多个请求等待获取锁的时候一旦占有锁的线程释放之后如果所有等待的方都同时被唤醒尝试抢占锁。但是这样的情况会造成比较大的开销那么在实现分布式锁的时候应该尽量避免惊群效应的产生。 3.公平锁和非公平锁不同的需求可能需要不同的分布式锁。非公平锁普遍比公平锁开销小。但是业务需求如果必须要锁的竞争者按顺序获得锁那么就需要实现公平锁。 4.阻塞锁和自旋锁针对不同的使用场景阻塞锁和自旋锁的效率也会有所不同。阻塞锁会有上下文切换如果并发量比较高且临界区的操作耗时比较短那么造成的性能开销就比较大了。但是如果临界区操作耗时比较长一直保持自旋也会对CPU造成更大的负荷。 保留以上所有问题和条件我们接下来看一些比较典型的实现方案。 典型实现 ZooKeeper的实现 ZooKeeper以下简称“ZK”中有一种节点叫做顺序节点假如我们在/lock/目录下创建3个节点ZK集群会按照发起创建的顺序来创建节点节点分别为/lock/0000000001、/lock/0000000002、/lock/0000000003。 ZK中还有一种名为临时节点的节点临时节点由某个客户端创建当客户端与ZK集群断开连接则该节点自动被删除。EPHEMERAL_SEQUENTIAL为临时顺序节点。 根据ZK中节点是否存在可以作为分布式锁的锁状态以此来实现一个分布式锁下面是分布式锁的基本逻辑 客户端调用create()方法创建名为“/dlm-locks/lockname/lock-”的临时顺序节点。 客户端调用getChildren(“lockname”)方法来获取所有已经创建的子节点。 客户端获取到所有子节点path之后如果发现自己在步骤1中创建的节点是所有节点中序号最小的那么就认为这个客户端获得了锁。 如果创建的节点不是所有节点中需要最小的那么则监视比自己创建节点的序列号小的最大的节点进入等待。直到下次监视的子节点变更的时候再进行子节点的获取判断是否获取锁。 释放锁的过程相对比较简单就是删除自己创建的那个子节点即可不过也仍需要考虑删除节点失败等异常情况。 开源的基于ZK的Menagerie的源码就是一个典型的例子https://github.com/sfines/menagerie 。 Menagerie中的lock首先实现了可重入锁利用ThreadLocal存储进入的次数每次加锁次数加1每次解锁次数减1。如果判断出是当前线程持有锁就不用走获取锁的流程。 通过tryAcquireDistributed方法尝试获取锁循环判断前序节点是否存在如果存在则监视该节点并且返回获取失败。如果前序节点不存在则再判断更前一个节点。如果判断出自己是第一个节点则返回获取成功。 为了在别的线程占有锁的时候阻塞代码中使用JUC的condition来完成。如果获取尝试锁失败则进入等待且放弃localLock等待前序节点唤醒。而localLock是一个本地的公平锁使得condition可以公平的进行唤醒配合循环判断前序节点实现了一个公平锁。 这种实现方式非常类似于ReentrantLock的CHL队列而且zk的临时节点可以直接避免网络断开或主机宕机锁状态无法清除的问题顺序节点可以避免惊群效应。这些特性都使得利用ZK实现分布式锁成为了最普遍的方案之一。 Redis的实现 Redis的分布式缓存特性使其成为了分布式锁的一种基础实现。通过Redis中是否存在某个锁ID则可以判断是否上锁。为了保证判断锁是否存在的原子性保证只有一个线程获取同一把锁Redis有SETNX即SET if Not eXists和GETSET先写新值返回旧值原子性操作可以用于分辨是不是首次操作操作。 为了防止主机宕机或网络断开之后的死锁Redis没有ZK那种天然的实现方式只能依赖设置超时时间来规避。 以下是一种比较普遍但不太完善的Redis分布式锁的实现步骤与下图一一对应 线程A发送SETNX lock.orderid 尝试获得锁如果锁不存在则set并获得锁。 如果锁存在则再判断锁的值时间戳是否大于当前时间如果没有超时则等待一下再重试。 如果已经超时了在用GETSET lock.{orderid} 来尝试获取锁如果这时候拿到的时间戳仍旧超时则说明已经获得锁了。 如果在此之前另一个线程C快一步执行了上面的操作那么A拿到的时间戳是个未超时的值这时A没有如期获得锁需要再次等待或重试。 该实现还有一个需要考虑的问题是全局时钟问题由于生产环境主机时钟不能保证完全同步对时间戳的判断也可能会产生误差。 以上是Redis的一种常见的实现方式除此以外还可以用SETNXEXPIRE来实现。Redisson是一个官方推荐的Redis客户端并且实现了很多分布式的功能。它的分布式锁就提供了一种更完善的解决方案源码https://github.com/mrniko/redisson 。 Tair的实现 Tair和Redis的实现类似Tair客户端封装了一个expireLock的方法通过锁状态和过期时间戳来共同判断锁是否存在只有锁已经存在且没有过期的状态才判定为有锁状态。在有锁状态下不能加锁能通过大于或等于过期时间的时间戳进行解锁。 采用这样的方式可以不用在Value中存储时间戳并且保证了判断是否有锁的原子性。更值得注意的是由于超时时间是由Tair判断所以避免了不同主机时钟不一致的情况。 以上的几种分布式锁实现方式都是比较常见且有些已经在生产环境中应用。随着应用环境越来越复杂这些实现可能仍然会遇到一些挑战。 强依赖于外部组件分布式锁的实现都需要依赖于外部数据存储如ZK、Redis等等因此一旦这些外部组件出现故障那么分布式锁就不可用了。 无法完全满足需求不同分布式锁的实现都有相应的特点对于一些需求并不能很好的满足如实现公平锁、给等待锁加超时时间等等。 基于以上问题结合多种实现方式我们开发了Cerberus得名自希腊神话里守卫地狱的猛犬致力于提供灵活可靠的分布式锁。 Cerberus分布式锁 Cerberus有以下几个特点。 特点一一套接口多种引擎 Cerberus分布式锁使用了多种引擎实现方式Tair、ZK、未来支持Redis支持使用方自主选择所需的一种或多种引擎。这样可以结合引擎特点选择符合实际业务需求和系统架构的方式。 Cerberus分布式锁将不同引擎的接口抽象为一套屏蔽了不同引擎的实现细节。使得使用方可以专注于业务逻辑也可以任意选择并切换引擎而不必更改任何的业务代码。 如果使用方选择了一种以上的引擎那么以配置顺序来区分主副引擎。以下是使用主引擎的推荐 功能需求TairZK并发量高✔响应时间敏感✔临界区执行时间长✔公平锁✔非公平锁✔读写锁✔ 特点二使用灵活、学习成本低 下面是Cerberus的lock方法这些方法和JUC的ReentrantLock的方式保持一致使用非常灵活且不需要额外的学习时间。 void lock(); 获取锁如果锁被占用将禁用当前线程并且在获得锁之前该线程将一直处于阻塞状态。 boolean tryLock(); 仅在调用时锁为空闲状态才获取该锁。 如果锁可用则获取锁并立即返回值 true。如果锁不可用则此方法将立即返回值 false。 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 如果锁在给定的等待时间内空闲并且当前线程未被中断则获取锁。 如果在给定时间内锁可用则获取锁并立即返回值 true。如果在给定时间内锁一直不可用则此方法将立即返回值false。 void lockInterruptibly() throws InterruptedException; 获取锁如果锁被占用则一直等待直到线程被中断或者获取到锁。 void unlock(); 释放当前持有的锁。 特点三支持一键降级 Cerberus提供了实时切换引擎的接口: String switchEngine() 转换分布式锁引擎按配置的引擎的顺序循环转换。 返回值返回当前的engine名字如”zk”。 String switchEngine(String engineName) 转换分布式锁引擎切换为指定的引擎。 参数engineName - 引擎的名字同配置bean的名字”zk”/“tair”。 返回值返回当前的engine名字如”zk”。 当使用方选择了两种引擎平时分布式锁会工作在主引擎上。一旦所依赖的主引擎出现故障那么使用方可以通过自动或者手动方式调用该切换引擎接口平滑的将分布式锁切换到另一个引擎上以将风险降到最低。自动切换方式可以利用Hystrix实现。手动切换推荐的一个方案则是使用美团点评基于Zookeeper的基础组件MCC通过监听MCC配置项更改来达到手动将分布式系统所有主机同步切换引擎的目的。需要注意的是切换引擎目前并不会迁移原引擎已有的锁。这样做的目的是出于必要性、系统复杂度和可靠性的综合考虑。在实际情况下引擎故障到切换引擎尤其是手动切换引擎的时间要远大于分布式锁的存活时间。作为较轻量级的Cerberus来说迁移锁会带来不必要的开销以及较高的系统复杂度。鉴于此如果想要保证在引擎故障后的绝对可靠那么则需要结合其他方案来进行处理。 除此以外Cerberus还提供了内置公用集群免去搭建和配置集群的烦恼。Cerberus也有一套完善的应用授权机制以此防止业务方未经评估使用对集群造成影响。 目前Cerberus分布式锁已经持续迭代了8个版本先后在美团点评多个项目中稳定运行。 所谓幂等简单地说就是对接口的多次调用所产生的结果和调用一次是一致的。扩展一下这里的接口可以理解为对外发布的HTTP接口或者Thrift接口也可以是接收消息的内部接口甚至是一个内部方法或操作。 那么我们为什么需要接口具有幂等性呢设想一下以下情形 在App中下订单的时候点击确认之后没反应就又点击了几次。在这种情况下如果无法保证该接口的幂等性那么将会出现重复下单问题。 在接收消息的时候消息推送重复。如果处理消息的接口无法保证幂等那么重复消费消息产生的影响可能会非常大。 在分布式环境中网络环境更加复杂因前端操作抖动、网络故障、消息重复、响应速度慢等原因对接口的重复调用概率会比集中式环境下更大尤其是重复消息在分布式环境中很难避免。Tyler Treat也在《You Cannot Have Exactly-Once Delivery》一文中提到 Within the context of a distributed system, you cannot have exactly-once message delivery. 分布式环境中有些接口是天然保证幂等性的如查询操作。有些对数据的修改是一个常量并且无其他记录和操作那也可以说是具有幂等性的。其他情况下所有涉及对数据的修改、状态的变更就都有必要防止重复性操作的发生。通过间接的实现接口的幂等性来防止重复操作所带来的影响成为了一种有效的解决方案。 GTIS GTIS就是这样的一个解决方案。它是一个轻量的重复操作关卡系统它能够确保在分布式环境中操作的唯一性。我们可以用它来间接保证每个操作的幂等性。它具有如下特点 高效低延时单个方法平均响应时间在2ms内几乎不会对业务造成影响 可靠提供降级策略以应对外部存储引擎故障所造成的影响提供应用鉴权提供集群配置自定义降低不同业务之间的干扰 简单接入简捷方便学习成本低。只需简单的配置在代码中进行两个方法的调用即可完成所有的接入工作 灵活提供多种接口参数、使用策略以满足不同的业务需求。 实现原理-基本原理 GTIS的实现思路是将每一个不同的业务操作赋予其唯一性。这个唯一性是通过对不同操作所对应的唯一的内容特性生成一个唯一的全局ID来实现的。基本原则为相同的操作生成相同的全局ID不同的操作生成不同的全局ID。 生成的全局ID需要存储在外部存储引擎中数据库、Redis亦或是Tair等等均可实现。考虑到Tair天生分布式和持久化的优势目前的GTIS存储在Tair中。其相应的key和value如下 key将对于不同的业务采用APP_KEY业务操作内容特性生成一个唯一标识trans_contents。然后对唯一标识进行加密生成全局ID作为Key。 valuecurrent_timestamp trans_contentscurrent_timestamp用于标识当前的操作线程。 判断是否重复主要利用Tair的SETNX方法如果原来没有值则set且返回成功如果已经有值则返回失败。 实现原理-内部流程 GTIS的内部实现流程为 1.业务方在业务操作之前生成一个能够唯一标识该操作的transContents传入GTIS 2.GTIS根据传入的transContents用MD5生成全局ID 3.GTIS将全局ID作为keycurrent_timestamptransContents作为value放入Tair进行setNx将结果返回给业务方 4.业务方根据返回结果确定能否开始进行业务操作 5.若能开始进行操作若不能则结束当前操作 6.业务方将操作结果和请求结果传入GTIS系统进行一次请求结果的检验 7.若该次操作成功GTIS根据key取出value值跟传入的返回结果进行比对如果两者相等则将该全局ID的过期时间改为较长时间 8.GTIS返回最终结果。 实现原理-实现难点 GTIS的实现难点在于如何保证其判断重复的可靠性。由于分布式环境的复杂度和业务操作的不确定性在上一章节分布式锁的实现中考虑的网络断开或主机宕机等等问题同样需要在GTIS中设法解决。这里列出几个典型的场景 如果操作执行失败理想的情况应该是另一个相同的操作可以立即进行。因此需要对业务方的操作结果进行判断如果操作失败那么就需要立即删除该全局ID 如果操作超时或主机宕机当前的操作无法告知GTIS操作是否成功。那么我们必须引入超时机制一旦长时间获取不到业务方的操作反馈那么也需要该全局ID失效 结合上两个场景既然全局ID会失效并且可能会被删除那就需要保证删除的不是另一个相同操作的全局ID。这就需要将特殊的标识记录下来并由此来判断。这里所用的标识为当前时间戳。 可以看到解决这些问题的思路也和上一章节中的实现有很多类似的地方。除此以外还有更多的场景需要考虑和解决所有分支流程如下: 使用说明 使用时业务方只需要在操作的前后调用GTIS的前置方法和后置方法如下图所示。如果前置方法返回可进行操作则说明此时无重复操作可以进行。否则则直接结束操作。 使用方需要考虑的主要是下面两个参数 空间全局性业务方输入的能够标志操作唯一性的内容特性可以是唯一性的String类型的ID也可以是map、POJO等形式。如订单ID等 时间全局性确定在多长时间内不允许重复1小时内还是一个月内亦或是永久。 此外GTIS还提供了不同的故障处理策略和重试机制以此来降低外部存储引擎异常对系统造成的影响。 目前GTIS已经持续迭代了7个版本距离第一个版本有近1年之久先后在美团点评多个项目中稳定运行。 在分布式环境中操作互斥性问题和幂等性问题非常普遍。经过分析我们找出了解决这两个问题的基本思路和实现原理给出了具体的解决方案。 针对操作互斥性问题常见的做法便是通过分布式锁来处理对共享资源的抢占。分布式锁的实现很大程度借鉴了多线程和多进程环境中的互斥锁的实现原理。只要满足一些存储方面的基本条件并且能够解决如网络断开等异常情况那么就可以实现一个分布式锁。目前已经有基于Zookeeper和Redis等存储引擎的比较典型的分布式锁实现。但是由于单存储引擎的局限我们开发了基于ZooKeeper和Tair的多引擎分布式锁Cerberus它具有使用灵活方便等诸多优点还提供了完善的一键降级方案。 针对操作幂等性问题我们可以通过防止重复操作来间接的实现接口的幂等性。GTIS提供了一套可靠的解决方法依赖于存储引擎通过对不同操作所对应的唯一的内容特性生成一个唯一的全局ID来防止操作重复。 目前Cerberus分布式锁、GTIS都已应用在生产环境并平稳运行。两者提供的解决方案已经能够解决大多数分布式环境中的操作互斥性和幂等性的问题。值得一提的是分布式锁和GTIS都不是万能的它们对外部存储系统的强依赖使得在环境不那么稳定的情况下对可靠性会造成一定的影响。在并发量过高的情况下如果不能很好的控制锁的粒度那么使用分布式锁也是不太合适的。总的来说分布式环境下的业务场景纷繁复杂要解决互斥性和幂等性问题还需要结合当前系统架构、业务需求和未来演进综合考虑。Cerberus分布式锁和GTIS也会持续不断地迭代更新提供更多的引擎选择、更高效可靠的实现方式、更简捷的接入流程以期满足更复杂的使用场景和业务需求。
http://www.zqtcl.cn/news/984667/

相关文章:

  • 知名网站建设公司电话做一个小程序需要多少钱
  • 外贸找客户有什么网站个人如何做网站
  • 旅游项目网站开发ui界面设计分析
  • 企业建设网站没有服务器代理网页浏览
  • 深圳网站建设新闻网站建设营销的企业
  • 建筑设计网站软件排名工具
  • wordpress theme珠宝最适合seo的网站源码
  • 建设工程规划许可证查询网站畜牧业网站模板
  • 做网站大概要多少钱页面网站缓存如何做
  • 家电网站建设需求分析朔州网络推广
  • 陕西交通建设集团网站体检网络营销中自建网站
  • 做游戏的php网站有哪些微信商城是什么
  • wordpress memcached redux深圳网站优化方法
  • 移动商城 网站建设方法方式韩国导航地图app
  • 企业网站源码是什么瑞安企业做网站
  • 佛山深圳建网站wordpress 段代码
  • 网站备案 强制仿牌网站容易被攻击吗
  • 网站做访问追踪js特效演示网站
  • 建设网站女装名字大全宝宝投票网站怎么做
  • 江苏省建设厅网站首页天津百度网站排名优化
  • 织梦网络设计工作室网站模板镇江市精神文明建设网站
  • 网站管理工具装修公司设计软件有哪些
  • 招标网站的服务费怎么做分录什么网站做玩具的比较多
  • 青海省住房建设厅网站WordPress主题启用出现错误
  • 自己怎么建网站网站的seo 如何优化
  • 博客网站模板下载如何自学美工
  • 哪个免费建站好专业seo要多少钱
  • 做3d建模贴图找哪个网站珠海建设网站公司简介
  • 网站开发过程前端后端qq刷赞网站咋做
  • 湘潭高新区建设局网站旅游做攻略的网站有哪些