一、引言

在当今数字化的浪潮中,分布式系统已成为众多企业构建大规模应用的首选架构。在分布式环境下,多个服务实例并行处理请求,当涉及到对共享资源的操作时,如创建订单,若缺乏有效的同步机制,就极易引发数据不一致、重复提交等棘手问题。分布式锁作为解决这些问题的关键技术,能确保在分布式环境下对共享资源的互斥访问。Redis凭借其高性能和支持原子操作的特性,成为实现分布式锁的热门选择。接下来,我们将深入剖析Redis分布式锁在创建海外仓头程订单代码中的应用原理。

二、分布式系统与锁的需求

分布式系统的挑战

分布式系统由多个独立的服务实例构成,这些实例可能部署在不同的服务器上,通过网络进行通信协作。在这样的环境中,多个实例可能会同时请求对同一个资源进行操作。例如,多个用户可能同时提交创建海外仓头程订单的请求,若没有适当的同步机制,就可能导致订单重复创建、数据冲突等问题,严重影响系统的正确性和稳定性。

锁的作用

锁是一种重要的同步机制,用于控制对共享资源的访问。在单进程环境中,我们可以使用语言提供的锁机制(如Java的 synchronized 关键字)来实现互斥访问。但在分布式系统中,这些本地锁机制不再适用,因为不同的服务实例运行在不同的进程中。因此,我们需要一种跨进程、跨服务器的分布式锁来确保对共享资源的互斥访问。

三、Redis分布式锁相关工具函数解析

生成唯一Token标识

private function generateToken()
{
    return uniqid(mt_rand(), true); // 高精度唯一值
}

此函数的核心作用是生成一个唯一的Token标识。uniqid 函数基于当前时间的微秒级别生成一个唯一ID,mt_rand() 作为前缀进一步增加了随机性,true 参数则确保生成更高精度的唯一值。这个唯一的Token将在后续的分布式锁操作中发挥关键作用,它能确保每个锁请求都有独一无二的标识,避免不同请求之间的混淆。

获取锁的key

public function getLockCacheKey($key)
{
    return "lock_{$key}";
}

该函数用于生成锁的缓存键。通过在传入的 key 前添加 lock_ 前缀,使得锁的键名具有明确的规范性和辨识度,方便在Redis中进行管理和区分。这样做可以避免不同业务场景下的锁键名冲突,提高系统的可维护性。

四、Redis分布式锁的原理

原子操作的重要性

Redis是一个高性能的键值对存储数据库,支持多种数据结构和操作。实现分布式锁的关键在于利用Redis的原子操作。原子操作是指在执行过程中不会被其他操作中断的操作,确保在多线程或多进程环境下的一致性。例如,Redis的 SET 命令可以在一个操作中完成设置键值和检查键是否存在的功能,避免了多个操作之间的竞争条件。

获取锁的原理

在代码中,获取分布式锁的核心是 getLock 方法:

public function getLock(string $key, int $time_out = NULL)
{
    $token = $this->generateToken();
    $timeoutMs = ($time_out ?? $this->time_out) * 1000; // 转毫秒

    //原子性操作:SET key=token NX PX=timeout
    $result = $this->handle->set(
        $key,
        $token,
        ['NX', 'PX' => $timeoutMs]
    );
    return $result ? $token : false;
}
  • 生成唯一Token:调用前面提到的 generateToken 方法生成一个唯一的Token,用于标识当前的锁请求。这个Token将作为锁的值存储在Redis中。
  • 设置超时时间:将传入的超时时间(秒)转换为毫秒,使用 PX 选项设置键的过期时间。这样可以避免因程序异常或其他原因导致锁无法释放,造成死锁的问题。
  • 原子性设置:使用 SET 命令的 NX 选项,表示只有当键不存在时才进行设置。如果设置成功,说明获取锁成功,返回生成的Token;否则返回 false

释放锁的原理

释放分布式锁使用了Lua脚本,以确保操作的原子性:

public function releaseLock(string $key, $token)
{
    $luaScript = <<<LUA
        if redis.call("get",KEYS[1]) == ARGV[1] then
            return redis.call("DEL",KEYS[1])
        else
            return 0
        end
    LUA;
    // 参数说明: 脚本,[KEYS, ARGV], KEY数量
    $result = $this->handle->eval($luaScript, [$key, $token], 1);
    return $result === 1;
}
  • 检查Token:首先通过 redis.call("get",KEYS[1]) 获取当前锁的Token,然后与传入的Token进行比较。只有当两者相等时,才说明当前请求有权释放该锁。
  • 删除锁:如果Token匹配,调用 redis.call("DEL",KEYS[1]) 删除该锁。使用Lua脚本可以确保这两个操作在一个原子操作中完成,避免了在检查Token和删除锁之间出现其他线程获取锁的情况。

五、代码中的具体应用

创建订单的流程

public function createFirstOrder()
{
    $redis = new Redis();
    $lock_key = $redis->getLockCacheKey('create_first_order_' . $this->info['membe']['membe_id']);

    $token = $redis->getLock($lock_key, 10);
    if (!$token) throw new ValidateException('请勿重复提交!');

    try {
        $params = $this->request->post();

        //创建海外仓头程订单
        $res = OverseaFirstOrderService::getInstance()->create_first_order($params);

        return success($res['data'], $res['msg'], $res['status']);
    } catch (ValidateException $e) {
        throw new ValidateException($e->getMessage());
    } finally {
        //无论如何都尝试释放锁
        $redis->releaseLock($lock_key, $token);
    }
}
  • 生成锁的键名:根据用户ID生成唯一的锁键名,调用 getLockCacheKey 方法确保每个用户的订单创建操作有独立的锁。
  • 获取锁:调用 getLock 方法尝试获取锁,设置超时时间为10秒。如果获取锁失败,说明有其他请求正在处理该用户的订单,抛出异常提示用户请勿重复提交。
  • 创建订单:在获取锁成功后,获取用户提交的参数,调用 OverseaFirstOrderService 类的 create_first_order 方法创建订单。
  • 释放锁:无论订单创建是否成功,在 finally 块中都调用 releaseLock 方法释放锁,确保锁资源被正确释放。

六、总结

Redis分布式锁在创建海外仓头程订单的代码中发挥了至关重要的作用。通过利用Redis的原子操作和Lua脚本的原子性,配合生成唯一Token和规范锁键名的工具函数,确保了在分布式环境下对订单创建操作的互斥访问。它有效避免了重复提交订单的问题,提高了系统的稳定性和数据的一致性。在实际开发中,使用Redis分布式锁需要注意锁的超时时间设置、锁的释放等问题,以确保系统的性能和正确性。同时,我们也应该认识到分布式锁只是解决分布式系统并发问题的一种手段,还需要结合其他技术和策略来构建更加健壮的分布式系统。


白穹雨
31 声望1 粉丝

热爱技术,热爱生活。学过Java,现在从事PHP。