基于Redis实现一个高性能的分布式锁

90 阅读3分钟

前言

在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足。简单来说,锁的本质是将并行变为串行,其主要目的是为了解决并发场景下导致数据错误,分布式锁是在多进程的场景下,保证业务串行。

举个最简单的库存案例来说明,先看如下伪代码:

    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来保证原子性。