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

Laravel框架
369
0
0
2022-11-16
分布式锁在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层,提升整体的系统吞吐量。