前言
在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足。简单来说,锁的本质是将并行变为串行,其主要目的是为了解决并发场景下导致数据错误,分布式锁是在多进程的场景下,保证业务串行。
举个最简单的库存案例来说明,先看如下伪代码:
func decr_stock(itemid,uid, num): bool {
val stock = stock_db.find_stock_by_id(itemid) // 步骤1. 从db查询剩余库存
if (stock >= num) { // 步骤2.判断库存是否充足
stock_db.decr_num(num) // 步骤3.扣减库存
return true
}
return false
}
一般扣减库存的逻辑是:先查询库存数量,判断是否满足条件,然后决定是否扣减库存(当然,如果你的数据库是mongodb,可以利用mongodb的find_and_modify incs解决并发,这里先不讨论,有兴趣的自行了解)。
上面这段代码,最大的问题就是,如果当前库存为1,存在A,B两个线程同时执行步骤1(假设A、B均扣减1个库存),那么将会导致库存数变为-1,即超卖。而解决办法就是加锁,如果是单节点,加一个进程内部的锁即可,而如果是多进程,则需要使用分布式锁保证正确性。
常见的分布式锁
- redis
- 数据库
- zookeeper
比较:
- redis,可靠性没有zk好,适用并发量比较大,高性能场景,目前我们生产环境就是使用redis实现分布式锁。
- 数据库锁,性能比较差,一般不会选择,但是一些项目没有引入redis和zk这样的中间件,为了保证程序正确,也只能选择这种方案
- zk锁,zk集群保证了可靠性,zk不支持分片以及使用长连接方式实现分布式锁,导致性能一般,适用于高可靠但是并发量不大的场景。
扯远了,至于数据库和zk的方案,这里先不谈。
分布式锁设计原则
- 互斥:最基本的原则,即保证同一个时间点,只有一个进程内的线程持有锁。
- 避免死锁:通常给锁设置一个失效时间。
- 故障迁移,即不会有单点故障。
加锁
下面是一段实现加锁的python代码:
def acquire(self, blocking=None, blocking_timeout=None):
sleep = self.sleep
token = b(uuid.uuid1().hex)
if blocking is None:
blocking = self.blocking
if blocking_timeout is None:
blocking_timeout = self.blocking_timeout
stop_trying_at = None
if blocking_timeout is not None:
stop_trying_at = mod_time.time() + blocking_timeout
while 1:
if self.do_acquire(token):
self.local.token = token
return True
if not blocking:
return False
if stop_trying_at is not None and mod_time.time() > stop_trying_at:
return False
mod_time.sleep(sleep)
def do_acquire(self, token):
if self.redis.setnx(self.name, token):
if self.timeout:
# convert to milliseconds
timeout = int(self.timeout * 1000)
self.redis.pexpire(self.name, timeout)
return True
return False
释放锁
def release(self):
expected_token = self.local.token
if expected_token is None:
raise LockError("Cannot release an unlocked lock")
self.local.token = None
self.do_release(expected_token)
def do_release(self, expected_token):
name = self.name
def execute_release(pipe):
lock_value = pipe.get(name)
if lock_value != expected_token:
raise LockError("Cannot release a lock that's no longer owned")
pipe.delete(name)
说明:redis使用setnx命令来加锁,注意锁需要设置过期时间,释放锁时可以通过LUA脚本和pipe来保证原子性。