欢迎跳转到本文的原文链接:https://honeypps.com/architect/distribute-lock-based-on-database/
参照连接 https://blog.csdn.net/u013256816/article/details/92854794
概述
在单机时代,虽然不需要分布式锁,但也面临过类似的问题,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就立即对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。
但是到了分布式系统的时代,这种线程之间的锁机制,就没作用了,系统可能会有多份并且部署在不同的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源。
因此,为了解决这个问题,我们就必须引入分布式锁。分布式锁是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。
目前比较常见的分布式锁实现方案有以下几种:
- 基于数据库,如MySQL
- 基于缓存,如Redis
- 基于Zookeeper、etcd等
我们在讨论使用分布式锁的时候往往首先排除掉基于数据库的方案,本能的会觉得这个方案不够“高级”。从性能的角度考虑,基于数据库的方案性能确实不够优异,整体性能对比:缓存 > Zookeeper、etcd > 数据库。也有人提出基于数据库的方案问题很多,不太可靠。笔者认为采用哪种方案是要基于使用场景来看的,选择哪种方案,合适最重要。
我这里引用一下之前文章中的一个应用场景——分配任务场景。在这个场景中,由于是公司的业务后台系统,主要是用于审核人员的审核工作,并发量并不是很高,而且任务的分配规则设计成了通过审核人员每次主动的请求拉取,然后服务端从任务池中随机的选取任务进行分配。这个场景看到这里你会觉得比较单一,但是实际的分配过程中,由于涉及到了按用户聚类的问题,所以要比我描述的复杂,但是这里为了说明问题,大家可以把问题简单化理解。那么在使用过程中,主要是为了避免同一个任务同时被两个审核人员获取到的问题。在这个场景下使用基于数据库的方案就比较合理。
再补充一下,比如某一个服务它下游依赖数据库来做一些数据的读写操作,模型如下图所示:
一般服务也是多实例部署,如果多个实例需要操作同一份数据的时候(比如前面所说的同一个任务同时被两个审核人员获取到的问题),自然而然的引入了分布式锁。不过此时,我们并没有采用数据库的方案,而是引入了Redis,模型如下图所示:
引入Redis之后,正向的收益我就不赘述了,反向的收益是增加了系统的复杂度,对于整个服务而言,还需要多考虑1和2失效的情况。1失效是指服务模块与Redis的交互出现了异常,这种异常不单是指无法通信的异常,也有可能是服务模块发送请求只Redis的过程中或者Redis响应服务模块的过程中出现的异常,整体服务需要考虑这种情况:是重试、丢弃还是采取其他措施;2失效是指Redis本身出现了异常。数据链路一旦变长,系统复杂度一旦变大,在出现问题的时候会阻碍故障排查以及服务恢复,从而使得服务的整体可用性下调。
反观,如果采用数据库的方案,那么就可以省去了这部分的复杂度,如果数据库的方案能满足当下场景以及可视范围内的未来扩展,为什么还要平白地增加系统复杂度呢?大家要根据具体业务场景选择合适的技术方案,而不是随便找一个足够复杂、足够新潮的技术方案来解决业务问题。
下面我们来了解一下基于数据库(MySQL)的方案,一般分为3类:基于表记录、乐观锁和悲观锁。
基于表记录
要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们想要获得锁的时候,就可以在该表中增加一条记录,想要释放锁的时候就删除这条记录。
为了更好的演示,我们先创建一张数据库表,参考如下:
class Flock(models.Model): id = models.AutoField(primary_key=True) user_id=models.IntegerField() # 设置唯一索引 class Meta: db_table='flock'
from django.db import connection #锁的使用 class Testlock(APIView): def post(self,request): uid=jwt_end(request.GET.get('jwt',None)) number=request.GET.get('number') res=User.objects.get(pk=uid) if res.balance>0: with connection.cursor() as a: a.execute("INSERT INTO flock(user_id) VALUES (%s)"%res.id) # try: # Flock.objects.create(user_id=res.id) # except Exception as e: # return Response({'msg':"无法操作"}) with connection.cursor() as c: c.execute('update user set balance=balance-%s where id=%s'%(int(number),res.id)) with connection.cursor() as w: w.execute("DELETE FROM flock WHERE user_id=%s"%res.id) return Response({'msg':"ok"}) else: return Response({'msg':"钱包为空"})