摘自:
https://www.cnblogs.com/seesun2012/p/9214653.html
一、分布式
1、CAP理论
任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)、分期容错性(Partition tolerance)最多同时只能满足两项。
2、分布式
- 分布式不是多线程,而是多进程
- 多线程可以共享堆内存,将标记直接存储在内存就可以实现锁。而分布式运行的各个进程很有可能分布在不同的机器上。因此需要将标记存储在所有进程都能看到的地方。
二、分布式锁
- 当在分布式模型下,数据只有一份,因此需要利用锁技术控制对数据修改的进程数。
- 锁要保证进程之间可见,还要考虑网络之间的问题。
- 分布式锁可以将标记存在内存,但是内存是进程共享的内从,集群环境,例如Redis。还可以利用数据库,保证标记能互斥就可以。
三、需要的分布式锁
- 在分布式部署的应用集群环境中,同一个方法在同一时刻只能被一台机器中的一个线程调用。
- 这把锁是一把可重入锁(避免死锁)
- 有高可用的获取锁和释放锁功能
- 获取锁和释放锁的性能要好
四、基于数据库的分布式锁
思路:利用主键唯一的特性,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁的话,删除这条数据库记录即可。
上面这种简单的实现有以下几个问题:
- 这把锁强依赖数据库的可用性,如果数据库是单点的,那么一旦数据库挂掉,会导致业务系统不可用
- 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其它线程无法再获取该锁
- 这把锁是非阻赛的,一旦insert失败就会报错
- 这把锁是非重入的,同一个线程在没有释放锁之前,无法获得该锁。
- 非公平锁
- 采用主键防重,在大并发情况下会造成锁表的情况。
上述问题的解决:
- 增加备用数据库,一旦一个挂了就立即切换
- 对于没有失效时间的问题,用定时任务清理超时数据
- 非阻塞的,用while循环直到insert成功
- 非重入的,增加字段,记录主机信息和线程信息,如果主机信息和线程信息在下一次操作可以查到,那么就直接分配
- 非公平的,再建一张中间表,将等待锁的线程全部记录下来,并根据创建时间排序,先创建的先获取锁
- 最后一个问题,解决办法是在程序执行过程中生产主键。
五、基于Redis做分布式锁
1、基于setnx(),expire()法
- setnx(key, value),set if not exists,该方法是原子的。如果key不存在,则当前key设置成功,返回1。如果当前key已经存在,则设置当前key失败,返回0。
- expire(),设置过期时间
setnx()如果返回0则返回占位失败,如果返回1则占位失败。
expire()命令对lockkey设置超时时间,为的是避免死锁行为
执行完业务代码后可以通过delete来删除。
该方法存在死锁问题
2、setnx(),get(),getset()法
为了解决死锁问题,新方法
- setnx(lockkey, 当前时间+过期时间),1则获取锁成功,0则没有获取锁,转为2。
- get(lockkey)获取值oldExpireTime,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转为3
- 计算newExpireTime = 当前时间 + 过期时间,然后getset(lockkey, newExpireTime)会返回当前lockkey的值currentExpireTime
- 判断currentExpireTime与oldExpireTime是否相等,如果相等,说明当前getset()设置成功,获取了锁。如果不等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
- 在获取锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再对锁进行处理