高并发场景下的解决方案以及分布式锁的实现

背景

在项目中经常会遇到并发场景,比如最典型的秒杀场景,假设后端有一张库存表,当大批请求过来时,都会经历读库存+减库存两个步骤,比如库存为100,两个线程先分别读到了库存100,然后各自执行减1并写回数据库,结果数据库数据为99而不是预期的98,这种情况如果不加以技术手段进行处理,很容易导致库存超卖。

另一个场景的例子是本人之前做过的请假系统,后端有一张表存储着请员工的可支配假期天数,当有多条请假申请被同时审批时,它们都要去执行读假期+减假期的操作,如果两个线程都先读到了假期数据,然后分别执行减假期的操作,就很容易导致假期少扣的情况。

我将对这一开发中的典型场景总结一些我自己的思考。

思路一:SQL优化

为了具体模拟秒杀这一场景,假设库存表叫做stock,商品数量叫做num,有业务代码计算出的新数目为new_num=num-1,原来执行的SQL为:

update stock set num=new_num where id = id=#{id};

我们可以改进SQL,让数据库根据自己当前的值更新数据,比如写成下面这样:

update stock set num=num-1 where id = #{id};
复制代码

这样虽然看似解决了问题,但是假设此时库存的数量为1,两个线程各自减1后,会发现库存被扣成了-1,同样也无法完全解决并发问题。

思路二:代码加锁

既然两个线程同时拿数据还是有问题,那么有没有办法让它们依次执行,于是很容易想到在查库存之前先加上锁,比如Java的synchronized关键字。

这能很好的保证串行,保证每次只有一个线程去执行查库存和扣库存的代码段,但同样缺点也非常多:

  1. 无法做到细粒度的控制,如果有的人秒杀商品A、有的秒杀商品B,都要走秒杀方法,相互之间不冲突但是也只能串行执行,访问会变得很慢。
  2. 因为spring里的@service是单例的,所以可以这么写,如果用python语言的django框架,则没法在代码层做类似的控制,不通用。
  3. 只支持单点(单机、服务器环境),无法做到水平扩展,现在基本都是集群部署的,仅能一台机器生效。

也许,我们需要一种第三方的机制,去实现这样的一把锁。

思路三:基于MySQL的悲观锁

因为所有的线程都在访问同一张表,可以在数据库上加锁,典型的做法有悲观锁和乐观锁两种。

悲观锁利用了select…for update 语法,例如:

select * from stock where id=#{id} for update;

注意:使用悲观锁要把业务代码放在事务里。

悲观锁获取数据时对数据行了锁定,其他事务要想获取锁,必须等原事务结束。

使用selec…for update会把数据给锁住,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。

使用悲观锁的缺点是,因为要锁表,所以并发性不高,如果并发过高,数据库压力过大,会宕机。

思路四:基于MySQL的乐观锁

乐观锁相对悲观锁

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值