关于分布式锁在编程中的一些应用场景

分布式锁在WEB编程中的应用相当广泛

以接口幂等性为例,未实现接口幂等性的注册接口,是这样的:

<?php

public function register(Request $request) 
{
    //一些验证逻辑

    $user = new User();
    $user->username = $request->post('phone');
    $user->password = Hash::make($request->post('password'));
    $user->save();
}

这样写有什么问题?明显,如果同一时间以相同的请求数据请求该接口,可能会导致数据库有两条同样的记录。诚然,我们可以为Users表的username字段设置唯一索引,再catch异常来抛出友好错误提示。但,这里我想介绍另一种方法,用分布式锁,以username作为分布式锁key的构建。

public function register(Request $request) 
{
    //一些验证逻辑
    $phone = $request->post('phone');

    $lock = Cache::lock('register-user-' . $phone, 10);

    if ($lock->get()) {
        $user = new User();
        $user->username = $phone;
        $user->password = Hash::make($request->post('password'));
        $user->save();
        $lock->release();
    } else {
        //返回友好的错误信息
    }

}

同样的,也可以编写一个限制访问频次的中间件,以客户端ip、路由和参数生成唯一指纹。

class AccessLimit
{
    public  function  handle($request, Closure  $next)
    {
        $fingerprint = md5($request->ip() . $request->route()->getName() . var_export($request->all(), true));

        $lock = Cache::lock($fingerprint, 5);
        if ($lock->get()) {
            return  $next($request);
        }

        throw new \Exception('访问频繁');

    }
}

还有一些非电商场景下的秒杀交易,比如说虚拟币,挂单交易只允许被一个用户购买。
如果不处理并发问题,就会发生超卖的情况。

<?php

public function deal(Request $request)
{
    $dealId = $request->post('id');
    $deal = Deal::find($dealId);

    //一些验证操作

    if (is_null($deal->sell_time)) {    //状态判断逻辑
        DB::beginTransaction();

        try {
            //处理业务逻辑

            $deal->sell_time = date('Y-m-d H:i:s');
            $deal->save();

            DB::commit();
        } catch (\Exception $e) {
            DB::rollback();
        }


        return; //返回交易成功响应
    }

    throw new \Exception('交易失败响应');


}

这样写在高并发场景下有明显的问题,两个用户同时抢单时,状态判断逻辑同时成功,并执行到接下来的业务逻辑。怎么改进?用分布式锁。

<?php

public function deal(Request $request)
{
    $dealId = $request->post('id');

    $lock = Cache::lock('deal-' . $dealId, 10);

    if (!$lock->get()) {
        throw new \Exception('该挂单已交易完成');
    }
    $deal = Deal::find($dealId);

    //一些验证操作

    DB::beginTransaction();

    try {
        //处理业务逻辑

        if (!is_null($deal->sell_time)) {    //状态判断逻辑
            throw new \Exception('该挂单已被交易');
        }

        $deal->sell_time = date('Y-m-d H:i:s');
        $deal->save();

        DB::commit();
        //返回成功响应
    } catch (\Exception $e) {
        DB::rollback();
        //返回失败响应
    } finally {
        $lock->release();
    }


}

这样处理,即可以避免超卖,也可以避免请求频繁落到DB层,提升整体的系统吞吐量。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 4

对于最后一个问题,两个人抢单,一个人抢到,另一个人给提示:该挂单已被交易。但万一前面那个人交易失败,这个提示就不妥当了。

1年前 评论
坐忘 1年前

幂等是不是理解错了?以你第一个注册的代码例子。锁过期后,数据库不还是有两条相同username的记录(没有唯一索引时)。

1年前 评论
fireqong (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!