【悲观锁】和【乐观锁】来解决秒杀系统中的超卖问题

       我们知道在秒杀系统中肯定是会碰到超卖的问题的,原因就是高并发请求导致了数据库的脏读不可重复读,进而造成了超额用户下了订单。
       解决方法可以通过封锁协议在数据库端对操作进行加锁,进而提高事务的隔离级别,来到达可串行化调度(多个事务的并发执行是正确的,当且仅当其结果与按某一次序串行地执行这些事务时的结果相同。可串行化调度当然也保持数据库的一致状态)。
       但是我们通常更多把处理和压力放在后端,通过对业务加悲观锁或是在表中加校验字段构成乐观锁来解决超卖问题。

1. 悲观锁

       悲观锁的核心理念是“请求超了一切就完蛋了”、“打死也不能多放进来一个请求”。加了悲观锁之后的接口在服务器调度时会进行线程控制,即该接口同时至多只能被一个线程所控制,也就是只能同时被一个请求所访问,其他的请求都会处于阻塞状态,直至上一个请求处理结束,控制权被释放,下一个请求再进来,就像银行的柜台处理业务一样,都需要排队,一次一个人。
       该方法可以有效的解决超卖问题,每一次请求都可以保证数据库数据的一致性,但是缺点是给用户的体验很不好,之后的用户可能已经注定请求失败,还要等待很长一段时间。
       悲观锁的写法是要对加锁的业务接口的方法内加上同步代码块

    @GetMapping("/kill")
    public AjaxResponse kill(Integer id){
        AjaxResponse ajax = AjaxResponse.newSuccess();
        try {
            //悲观锁
            synchronized (this){
                //业务层的秒杀方法:包括减库存和下订单,成功下订单后返回订单id
                int orderId = stockOrderService.kill(id);
                ajax.setData("SUCCESS! " + orderId);
                return ajax;
            }
        }catch (Exception e){   //可以使用自定义异常捕获
            e.printStackTrace();
            return AjaxResponse.newInstance(500,e.getMessage());
        }
    }

       synchronized同步字段可以放在方法定义上,也可以放在方法内部,这里推荐在接口内部以添加同步代码块的方法加上悲观锁,出现异常可以方便的被自定义异常类捕获
       经Jmeter压力测试后结果无误,没有发生超卖现象。
       但需要强调的一点是synchronized不要和@Transactional一起使用,特别是不要在Service层的方法上加上synchronized,由于Service层的业务方法大多加有事务控制,和悲观锁联合使用的时候,悲观锁解锁的时间比事务提交的时间早,可能会导致少量请求在上一次事务未完全提交就进来,最终导致少量的超卖。所以尽量在接口中加上同步代码块来控制业务的访问

2.乐观锁

       乐观锁的核心理念就是“请求访问自管来,弄错一个算我输”,就像超市大减价,大妈疯狂涌入,最后把商品几乎全部掠空。
       乐观锁的实现原理是在要秒杀的商品中加上一个校验字段,每次减库存得到时候需要对比和一开始得到的校验值是否相同,如果相同表示此数据是干净的,可以对其修改,如果不同说明已经有人修改过了。
       讲个通俗一点的,还是超市大减价,第一批10个大妈一起冲了进来,每个大妈都在前台领了一个令牌,上面写了1(前台也会保留一份)。之后10个大妈便开始疯狂抢东西,其中一个大妈跑得快,拿到东西第一个回到了前台,和前台的一对比,前台也还是1,该大妈抢货成功,可以拿着东西走了,此时服务员把令牌上的数字改为了2。剩下9个大妈陆续回到前台,由于手中的令牌都是1,和前台的2不匹配,算抢货失败,最后空手而归。下一批10个大妈再此涌进来,拿到的令牌从2开始,依此类推。
       上面的例子只是方便理解,没有别的意思。。。。(doge)
       假设我们的商品有一个字段为库存count,还有一个字段为version用来校验,那么首先根据id查询此商品,得到库存count和version,之后要对库存减一,即count-1,但是要加上where条件,为 更新id = 此商品id and 该商品的version = 一开始获得的version。如果更新结果为true(或是影响的行数不为零),表明更新成功,之后即可下订单,否则抛出异常表示失败。

//Service层的秒杀业务
public Integer kill(Integer id) {
        //先查询改商品    
        Stock stock = stockService.getById(id);
        if(stock.getCount() <= 0){    //  <=0防止数据击穿
            log.info("库存不足");
            throw new RuntimeException("库存不足");
        }else {
            //更新库存,这里使用mybatis plus
            LambdaUpdateWrapper<Stock> updateWrapper = new LambdaUpdateWrapper<>();
            updateWrapper.setSql("sale = (sale + 1)");     //设置更新内容
            updateWrapper.setSql("version = (version + 1)");    //设置更新内容
            updateWrapper.eq(Stock::getId,stock.getId())     //判断条件
                    .eq(Stock::getVersion,stock.getVersion());   //判断条件
            boolean res = stockService.update(updateWrapper);   //得到结果
            if(!res){
                throw new RuntimeException("抢购失败");
            }
            //创建订单
            StockOrder stockOrder = new StockOrder();
            stockOrder.setSid(stock.getId()).setName(stock.getName()).setCreateTime(new Date());
            stockOrderService.save(stockOrder);
            return stockOrder.getId();
        }
    }

       以上就是通过悲观锁和乐观锁来解决秒杀系统中的超卖问题,当然这两个方法是解决改问题的核心,我们还可以用一些额外的方法来优化请求数量,减少并发,环节服务端的压力,例如使用令牌桶算法限流redis缓存限时消息队列存储成功请求,加快处理时间等。
       如果本文在哪些地方讲解有误,欢迎在下午评论指出~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342