1.前言
大多数互联网系统是分布式部署的,分布式部署解决了高并发高可用的问题,但是由此带来了数据一致性问题。
当某个资源在多系统之间,被共享操作的时候,为了保证这个资源数据是一致的,那么就必须要求在同一时刻只能被一个客户端操作,不能并发的执行,否者就会出现同一时刻有客户端写,别的客户端在读,两者访问到的数据就不一致了。
2.我们为什么需要分布式锁
在单机时代,虽然不需要分布式锁,但也面临过类似的问题,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就立即对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。
但是到了分布式系统的时代,这种线程之间的锁机制,就没作用了,应用程序会有多份,并且部署在不同的机器上,这些资源已经不是在同一进程的不同线程间共享,而是属于多进程之间共享的资源。
因此,为了解决这个问题,我们就必须引入「分布式锁」。
分布式锁,是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。
分布式锁要满足哪些要求呢?
排他性:在同一时间只会有一个客户端能获取到锁,其它客户端无法获取
避免死锁:这把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放)
高可用:获取或释放锁的机制必须高可用且性能佳
而且最好是可重入锁。
3.分布式锁的实现方式有哪些
目前主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:
基于数据库实现
基于Redis实现
基于ZooKeeper实现
无论哪种方式,其实都不完美,依旧要根据咱们业务的实际场景来选择。
方案1 基于数据库实现
基于数据库来做分布式锁的话,通常有两种做法:
基于数据库的乐观锁
基于数据库的悲观锁
我们先来看一下如何基于「乐观锁」来实现:
乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。
当我们要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。
乐观锁通常实现基于数据版本(version)的记录机制实现的,比如有一张红包表(t_bonus),有一个字段(left_count)记录礼物的剩余个数,用户每领取一个奖品,对应的left_count减1,在并发的情况下如何要保证left_count不为负数,乐观锁的实现方式为在红包表上添加一个版本号字段(version),默认为0。
异常实现流程
--可能会发生的异常情况--线程1查询,当前left_count为1,则有记录select * from t_bonus where id = 10001 and left_count > 0
--线程2查询,当前left_count为1,也有记录select * from t_bonus where id = 10001 and left_count > 0
--线程1完成领取记录,修改left_count为0,
update t_bonusset left_count = left_count - 1 where id = 10001
-- 线程2完成领取记录,修改left_count为-1,产生脏数据
update t_bonusset left_count = left_count - 1 where id = 10001
通过乐观锁实现
--添加版本号控制字段
ALTER TABLE table ADD COLUMN version INT DEFAULT'0'NOT NULL AFTER t_bonus;--线程1查询,当前left_count为1,则有记录,当前版本号为1234select left_count, version from t_bonus where id = 10001 and left_count > 0
--线程2查询,当前left_count为1,有记录,当前版本号为1234select left_count, version from t_bonus where id = 10001 and left_count > 0
--线程1,更新完成后当前的version为1235,update状态为1,更新成功
update t_bonusset version = 1235, left_count = left_count-1 where id = 10001 and version = 1234