1,数据一致性
当多个进程/线程对同一个共享资源读写,会因为资源的争夺而出现混乱,导致数据不一致。
如下图:
在数据库的原始数据是 d0,上图的处理流程如下:
- t1 时刻,有两个数据源的数据 d1,d2 分别到达数据处理层,主进程分配线程 Merge1 处理 d1,Merge2 处理 d2,两者又同时(假设还是 t1 )从数据库获取原始数据 d0
- t2 时刻,Merge1 合并完 d0 和 d1 的数据,并将合并后的数据存到数据库,数据库的数据变成 d0 + d1
- t3 时刻,Merge2 合并完 d0 和 d2 的数据,并将合并后的数据存到数据库,数据库的数据变成 d0 + d2
- t1 到 t3,数据库最终的数据变成了 d0 + d2,数据源 d1 的数据消失,出现数据不一致问题。
上面所列的问题,是由于多线程同时对某一个共享数据进行读写导致,我们只要找到一种方案,使得对共享数据的访问是同步的,即可解决该问题。当有某个线程或者进程已经访问了该数据,其他进程或者线程就必须等待其访问结束,才可拥有该共享数据的访问权(进入临界区)。最简单的方式,就是加个同步锁。
锁的实现方式,按照应用的实现架构,可能会有以下几种类型:
-
如果处理程序是单进程多线程的,在 python下,就可以使用 threading 模块的 Lock 对象来限制对共享变量的同步访问,实现线程安全。
-
单机多进程的情况,在 python 下,可以使用 multiprocessing 的 Lock 对象来处理。
-
多机多进程部署的情况,就得依赖一个第三方组件(存储锁对象)来实现一个分布式的同步锁了。
2,分布式锁实现方式
目前主流的分布式锁实现方式有以下几种:
-
基于数据库来实现,如 mysql
-
基于缓存来实现,如 redis
-
基于 zookeeper 来实现
下面我们简单介绍下这几种锁的实现。
2.1,基于数据库的锁:
基于数据库的锁实现也有两种方式,一是基于数据库表,另一种是基于数据库排他锁。
基于数据库表的增删:
基于数据库表增删是最简单的方式,首先创建一张锁的表主要包含下列字段:方法名,时间戳等字段。
具体使用的方法,当需要锁住某个方法时,往该表中插入一条相关的记录。这边需要注意,方法名是有唯一性约束的,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。执行完毕,需要delete该记录。
对于上述方案可以进行优化,如应用主从数据库,数据之间双向同步。一旦挂掉快速切换到备库上;做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍;使用while循环,直到insert成功再返回成功,虽然并不推荐这样做;还可以记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了,实现可重入锁。
数据库的排他锁:
基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作。
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。其他没有获取到锁的就会阻塞在上述select语句上,可能的结果有2种,在超时之前获取到了锁,在超时之前仍未获取到锁。
获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,释放锁 connection.commit()
。
存在的问题主要是性能不高和sql超时的异常。
2.2,基于zookeeper实现
基于zookeeper临时有序节点可以实现的分布式锁。每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
提供的第三方库有 curator ,具体使用读者可以自行去看一下。Curator提供的InterProcessMutex是分布式锁的实现。acquire方法获取锁,release方法释放锁。另外,锁释放、阻塞锁、可重入锁等问题都可以有有效解决。讲下阻塞锁的实现,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是就获取到锁,便可以执行业务逻辑。
最后,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。并发问题,可能存在网络抖动,客户端和ZK集群的session连接断了,zk集群以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。
用法参考:https://yunjianfei.iteye.com/blog/2164888
2.3,基于缓存redis实现
相对于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点,存取速度快很多。而且很多缓存是可以集群部署的,可以解决单点问题。
使用redis的SETNX实现分布式锁,多个进程执行以下Redis命令:
SETNX lock.id <current Unix time + lock timeout + 1>
SETNX是将 key 的值设为 value,当且仅当 ke