服务端通过nosql加锁解决并发问题实战并发问题的解决思路

「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

并发问题的解决思路

  1. 首先想到的就是加锁,比如mysql加锁,解决并发问题。这类文章很多,就不赘述了。
  2. 再者是nosql加锁,解决mysql加锁解决不了的情况,毕竟nosql的性能是比mysql等关系型数据库快很多的,nosql加锁的并发级别比mysql等关系数据库的要高。

nosql加锁的案例

  1. 我的业务场景是每天第一次登陆APP发优惠券
  2. 我在业务代码里已经添加了是否存在的判断,如果今天已经给同一用户在同一时间下发了相同来源的优惠券则不重复下发
  3. 因为业务需要是相同类型相同来源的优惠券可以给同一个人在同一时间下发多张,我们就不能用MySQL的唯一索引了
  4. 我们在【2】中提到的判断,只能对非并发情况的请求起到限制作用,并大请求是不起作用的。
  5. 这时候就可以使用nosql加锁的思路,比如使用redis锁:
    1. 在业务开始时加锁
    2. 执行业务代码
    3. 业务执行完毕释放锁
  6. 原理:nosql加锁的并发级别比mysql等关系数据库的要高。

示例代码

未加锁的代码

//每天第一次登陆发放预约券
public static function everydayTriggerCoupon($userid)
{
    $userVip = UserVip::getByUserid($userid, 'type');
    if ($userVip['type'] == VipInfo::TYPE_USER_NORMAL) {
        //非会员发放3张预约券
        if (!self::checkCouponExist($userid, CouponInfo::PROP_COUPON_SUBSCRIBE_APPOINTMENT_ID, CouponInfo::TYPE_COUPON_SOURCE_DAILY_BENEFITS, Utility::getTomorrowTimestamp())) {
            self::saveCoupon($userid, CouponInfo::PROP_COUPON_SUBSCRIBE_APPOINTMENT_ID, CouponInfo::TYPE_COUPON_SOURCE_DAILY_BENEFITS, Utility::getTomorrowTimestamp(), CouponInfo::COUNT_COUPON_EVERYDAY_TRIGGER_NOT_VIP);
        }
    } else {
        //会员发放5张预约券
        if (!self::checkCouponExist($userid, CouponInfo::PROP_COUPON_SUBSCRIBE_APPOINTMENT_ID, CouponInfo::TYPE_COUPON_SOURCE_DAILY_VIP_BENEFITS, Utility::getTomorrowTimestamp())) {
            self::saveCoupon($userid, CouponInfo::PROP_COUPON_SUBSCRIBE_APPOINTMENT_ID, CouponInfo::TYPE_COUPON_SOURCE_DAILY_VIP_BENEFITS, Utility::getTomorrowTimestamp(), CouponInfo::COUNT_COUPON_EVERYDAY_TRIGGER_VIP);
        }
        //会员额外发放1张超级喜欢券
        if (!self::checkCouponExist($userid, CouponInfo::PROP_COUPON_SUPER_LIKE_ID, CouponInfo::TYPE_COUPON_SOURCE_DAILY_VIP_BENEFITS, Utility::getTomorrowTimestamp())) {
            self::saveCoupon($userid, CouponInfo::PROP_COUPON_SUPER_LIKE_ID, CouponInfo::TYPE_COUPON_SOURCE_DAILY_VIP_BENEFITS, Utility::getTomorrowTimestamp());
        }
    }
}
复制代码

添加锁的代码

  1. 结合业务需求,每天第一次登陆发放优惠券,即当天有效,所以在设置key时把当前的日期作为其中一个参数
  2. 因为我使用的是Laravel框架,Cache集成了Redis,Cache的add方法和Redis的setnx方法类似,都有存在key值返回0,不存在key值返回1的特性。
  3. 结合我们的业务场景,我在add()的时候添加了24小时的过期时间;当然如果你们的场景对过期时间没有邀请,可以在业务代码执行完毕之后主动释放缓存资源。

注意:一定要记得在合适的场景下释放锁资源,避免资源滥用

//每天第一次登陆发放预约券
public static function everydayTriggerCoupon($userid)
{
    $userVip = UserVip::getByUserid($userid, 'type');
    //解决并发问题 加redis锁
    $cacheKey = CacheKey::getCacheKey(CacheKey::TYPE_USER_REWARD_EVERYDAY_TRIGGER, $userid . '_' . date('Y-m-d'));
    $res = Cache::add($cacheKey, true, 60 * 60 * 24); //add()不存在添加返回true; 存在返回false
    if ($res) {
        和未加锁的核心代码一致
        .
        .
        .
    }
}
复制代码

异步任务加nosql锁的注意问题

如果我们的异步任务出现了并发问题,也可以考虑通过引入nosql锁的方式解决。

这时候要注意:如果异步任务是单线程的,是按顺序执行的,要在调用异步任务的方法外加锁,不要加到异步任务方法里,避免造成阻塞。

示例代码

$lockKey = CacheKey::getCacheKey(CacheKey::TYPE_JOB_USER_EDIT_AVOID_CONCURRENT, $userid);
$lockRes = Cache::add($lockKey, true, 60);
if ($lockRes) {
UserInfoEdit::dispatch([
    'userid' => $userid,
    .
    .
    .
])->onQueue(QueueNameBuilder::getName(QueueNameBuilder::USER_INFO_EDIT));
//释放redis锁
    Cache::forget($lockKey);
}
复制代码

Last but not least

技术交流群请到 这里来。 或者添加我的微信 wangzhongyang0601 ,一起学习。

感谢大家的点赞、评论、关注,谢谢大佬们的支持,感谢。