精华内容
下载资源
问答
  • python 如何解决高并发下的库存问题

    千次阅读 2018-03-11 07:08:49
    一个简单的使用场景:一件商品的库存只有5件,同时A用户买了5个,B用户买了5个,都提交数据,照成库存不足的问题。 逻辑:根据一般电商商品的模型类,生成订单一般包括订单类...那么python如何解决库存问题呢? p...

            一个简单的使用场景:一件商品的库存只有5件,同时A用户买了5个,B用户买了5个,都提交数据,照成库存不足的问题。

            逻辑:根据一般电商商品的模型类,生成订单一般包括订单类(Order)和订单详情类(DetailOrder),这两张表根据外键order_id 进行关联,所以是同生共死的关系,所以我们在这里用事务来控制。那么python如何解决库存问题呢?

            python 提供了2种方法解决该问题的问题:1,悲观锁;2,乐观锁

            悲观锁:在查询商品储存的时候加锁 select_for_update()  在发生事务的commit或者是事务的rollback时,自动释放该锁,这样其他用户就可以接着查询该商品。

            乐观锁:乐观锁不是真正的锁,在创建订单之前查询商品的库存,在创建订单详情表前,update更新查询数据,如果两次查询的库存量一样就创建详情表,并减去库存,否则,循环三次,如果都不一样,就发生rollback。

            使用场景:并发量高的时候使用悲观锁,缺点:加锁消耗资源

                           并发量低的时候使用乐观锁,缺点:乐观锁循环耗费时间。

    代码:

    悲观锁:

     @transaction.atomic
        def post(self, request):
            """接受数据  修改购物车数据 添加订单 和订单商品 加悲观锁 解决并发问题"""
            sku_ids = request.POST.get('sku_ids')
            addr_id =request.POST.get('addr_id')
            pay_method = request.POST.get('pay_method')
            transit_price = request.POST.get('transit_price')
            user = request.user
            if not user.is_authenticated():  # 判断是否登陆
                return JsonResponse({'res': 0, 'errmsg': '用户未登录,请先登陆'})
            try:  # 判断地址是否存在
                address = Customer.objects.get(id=addr_id)
            except Customer.DoesNotExist:
                return JsonResponse({'res': 1, 'errmsg': '地址不存在'})

            #  判断支付方式是否存在
            if pay_method not in OrderInfo.PAY_METHODS.keys():
                return JsonResponse({'res': 2, 'errmsg': '支付方式不存在'})
            total_price = 0
            total_count = 0
            order_id = datetime.now().strftime('%Y%m%d%H%M%S') + str(user.id)
            # 设置事务保存点
            tip = transaction.savepoint()
            try:
                new_order =OrderInfo.objects.create(order_id=order_id,
                                         user=user,
                                         addr=address,
                                         pay_method=pay_method,
                                         total_count=total_count,
                                         total_price=total_price,
                                         transit_price=transit_price)
            except Exception as e:
                # 回滚到tip
                transaction.savepoint_rollback(tip)
                return JsonResponse({'res': 3, 'errmsg': '生成订单失败'})
            cart_key = 'cart_%d' % user.id
            conn = get_redis_connection('default')
            skus = conn.hgetall(cart_key)
            for sku_id in eval(sku_ids):  # 循环获得商品的信息  每条数据生成一个订单商品数据
                # 获得购物车中每个商品的数量
                sku_count = conn.hget(cart_key, sku_id)
                if not sku_count:  # 判断购物车里有没有该商品
                    # 回滚到tip
                    transaction.savepoint_rollback(tip)
                    return JsonResponse({'res': 4, 'errmsg': '该商品不在购物车中'})
                try:  # 在商品表中 找该商品sel
                    sku = GoodsSKU.objects.select_for_update().get(id=sku_id, is_show=1)
                except GoodsSKU.DoesNotExist:
                    # 回滚到tip
                    transaction.savepoint_rollback(tip)
                    return JsonResponse({'res': 5, 'errmsg': '该商品已下架'})
                price = sku.price
                sku_count = int(sku_count)
                if sku_count > int(sku.count):  # 判断库存
                    # 回滚到tip
                    transaction.savepoint_rollback(tip)
                    return JsonResponse({'res': 6, 'errmsg': '库存不足'})
                price = int(price)
                sku_price = sku_count * price
                total_price += sku_price
                total_count += sku_count
                # 添加订单商品表
                try:
                    OrderGoods.objects.create(order=new_order,
                                              sku=sku,
                                              count=sku_count,
                                              price=sku_price,
                                              )
                except Exception as e:
                    # 回滚到tip
                    transaction.savepoint_rollback(tip)
                    return JsonResponse({'res': 7, 'errmsg': '订单商品创建失败'})
                # 减少商品库存 增加商品销售额
                sku.count -= sku_count
                sku.sale_count += sku_count
                sku.save()
                # 删除修改购物车数据
                conn.hdel(cart_key, sku_id)
            total_price += int(transit_price)
            # 修改order 订单数量 总价  邮费
            new_order.total_count = total_count
            new_order.total_price = total_price
            new_order.transit_price = transit_price
            new_order.save()
            #释放保存点

            transaction.savepoint_commit(tip)

    乐观锁:

              @transaction.atomic
        def post(self, request):
            '''订单创建'''
            # 判断用户是否登录
            user = request.user
            if not user.is_authenticated():
                return JsonResponse({'res':0, 'errmsg':'用户未登录'})
            # 接收参数
            addr_id = request.POST.get('addr_id')
            pay_method = request.POST.get('pay_method')
            sku_ids = request.POST.get('sku_ids') # 1,4
            # 校验参数
            if not all([addr_id, pay_method, sku_ids]):
                return JsonResponse({'res':1, 'errmsg':'数据不完整'})
            # 校验地址
            try:
                addr = Address.objects.get(id=addr_id)
            except Address.DoesNotExist:
                # 地址不存在
                return JsonResponse({'res':2, 'errmsg':'地址信息错误'})
            # 校验支付方式
            if pay_method not in OrderInfo.PAY_METHODS.keys():
                # 支付方式无效
                return JsonResponse({'res':3, 'errmsg':'支付方式无效'})
            # 业务处理:订单创建
            # 组织参数
            # 订单id(order_id): 20171211123130+用户的id
            order_id = datetime.now().strftime('%Y%m%d%H%M%S')+str(user.id)
            # 运费
            transit_price = 10
            # 总数目和总金额
            total_count = 0
            total_price = 0
            # 设置保存点
            sid = transaction.savepoint()
            try:
                # todo: 向df_order_info表中添加一条记录
                order = OrderInfo.objects.create(order_id=order_id,
                                                 user=user,
                                                 addr=addr,
                                                 pay_method=pay_method,
                                                 total_count=total_count,
                                                 total_price=total_price,
                                                 transit_price=transit_price)
                conn = get_redis_connection('default')
                cart_key = 'cart_%d'%user.id
                # todo: 用户的订单中包含几个商品,就应该向df_order_goods中添加几条记录
                sku_ids = sku_ids.split(',') # [1,4]
                for sku_id in sku_ids:
                    for i in range(3):
                        # 根据sku_id获取商品的信息
                        try:
                            # select * from df_goods_sku where id=sku_id;
                            sku = GoodsSKU.objects.get(id=sku_id)
                        except GoodsSKU.DoesNotExist:
                            transaction.savepoint_rollback(sid)
                            return JsonResponse({'res':4, 'errmsg':'商品信息错误'})
                        # 获取用户要购买的商品的数目
                        count = conn.hget(cart_key, sku_id)
                        # todo: 判断商品库存
                        if int(count) > sku.stock:
                            transaction.savepoint_rollback(sid)
                            return JsonResponse({'res':6, 'errmsg':'商品库存不足'})
                        # todo: 减少商品的库存,增加销量
                        origin_stock = sku.stock
                        new_stock = origin_stock - int(count)
                        new_sales = sku.sales + int(count)
                        # print('user:%d i:%d stock:%d'%(user.id, i, origin_stock))
                        # import time
                        # time.sleep(10)
                        # 返回更新的行数
                        # update df_goods_sku set stock=new_stock, sales=new_sales
                        # where id=sku_id and stock=origin_stock
                        res = GoodsSKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales)
                        if res == 0:
                            # 更新失败
                            if i == 2:
                                # 已经尝试了3次,下单失败
                                transaction.savepoint_rollback(sid)
                                return JsonResponse({'res':7, 'errmsg':'下单失败2'})
                            continue
                        # todo: 向df_order_goods中添加一条记录
                        OrderGoods.objects.create(order=order,
                                                  sku=sku,
                                                  count=count,
                                                  price=sku.price)
                        # todo: 累加计算商品的总件数和总金额
                        total_count += int(count)
                        total_price += sku.price*int(count)
                        # 更新成功之后跳出循环
                        break
                # todo: 更新order的total_count和total_price
                order.total_count = total_count
                order.total_price = total_price
                order.save()
            except Exception as e:
                transaction.savepoint_rollback(sid)
                return JsonResponse({'res':7, 'errmsg':'下单失败'})
            # 释放保存点
            transaction.savepoint_commit(sid)
            # todo: 删除购物车对应记录信息
            conn.hdel(cart_key, *sku_ids) # 1,4
            # 返回应答
            return JsonResponse({'res':5, 'message':'订单创建成功'})

     tip:使用乐观锁时,记得修改mysql的隔离级别:vi /etc/mysql/mysql.conf.d/mysqld.cnf    在mysqld最下面一行添加:transaction-isolation = READ_COMMITTED 。

    展开全文
  • 库存可以采用同步调用(商品微服务提供接口,通过Feign调用),也可以采用异步...若库存不足,则减库存失败,但是订单微服务中并不知道减库存失败,因此事务不会回滚,这就是分布式事务问题 (跨服务的事务)。我...
      
    

    减库存可以采用同步调用(商品微服务提供接口,通过Feign调用),也可以采用异步调用(通过RabbitMq),我们采用同步调用,接下来我们来分析分析原因。

    若采用异步调用的方式,我们要进行减库存,只需向RabbitMq中发送消息即可,后续我们不管,但是否减库存成功我们不得而知。若库存不足,则减库存失败,但是订单微服务中并不知道减库存失败,因此事务不会回滚,这就是分布式事务问题 (跨服务的事务)。我们写的减库存业务从订单微服务跨越到了商品微服务,而事务是由Spring来管理的,两个tomcat两个Spring,两个事务本身没有任何关联,但是却是一个事务(减库存和创建订单应该一起完成),如果采用异步,一边微服务执行失败另一边微服务并不知道,破坏了事务的一致性

    我们把异步调用变成同步调用调用如果失败就会抛出异常,事务自然回滚(减库存操作只能放在创建订单业务的最后,因为减库存执行失败,事务自然会回滚,订单也就不会创建成功,但是如果刚开始就做减库存操作,那如果订单创建失败库存无法回滚,导致丢失库存数据),但当业务更复杂时(比如:优惠券功能、积分),有好几个服务之间调用。这时我们把那个放在最后呢?哪个放在最后都不行,这时候就要解决分布式事务问题了。


    解决分布式事务问题:
    • 2PC(两阶段提交):第一阶段(执行阶段):当“我”创建订单时,“我”向所有“我”要调用的微服务发一个请求,告诉他们开始执行,执行完毕后向“我”返回一条消息,告诉“我”你是执行成功了还是失败了;第二阶段:如果上一阶段所有人都执行成功,然后“我”会再发一条消息告诉所有人让其提交,如果第一阶段有一个人执行失败,则通知所有人都回滚

      • 实现复杂、还会用到一些异步通信的手段(比如:MQ),在业务执行过程中,数据锁定的范围太大(比如“我”要调用4个业务,即四个微服务,那四个微服务所对应的四张表全部锁定),在任何一个其他业务未执行完之前,所有的表都是锁定状态,因此在性能上较差,因此在电商行业里,这种互联网要求并发很高的团队里很少使用
    • TCC(try-confirm-cancel):编写任何一个与分布式事务相关的业务功能,必须写两个业务:一个是确认执行业务(confirm),另一个是取消执行业务(cancel,即补偿业务) 。比如说减库存这个业务,执行业务就是减库存,补偿业务就是加库存。基本原理:开始事务以后,让所有业务都开始执行,各完成各的,互相不等待,每个业务完成了就提交,这样就解决了两阶段提交时数据库被锁定的情况(互相要等待),但是如果A业务提交但B业务失败了呢?所以它有一个补偿业务(大家都去提交,提交完了之后统一再把结果返回给“我”,“我”看下是全部都成功了还是有失败的,如果都成功,就confirm提交;反之,“我”会执行调用提前写好的补偿业务,也就是说它不是通过回滚的方式,而是通过补偿的方式)

      • 可以解决两阶段提交的锁定问题
      • 但是业务变得复杂了,写一个业务时必须写一个确定执行业务方法和一个补偿业务方法,除此之外还要考虑补偿方案的失败问题,当补偿方案也执行失败了呢,这时候就要考虑重试问题、人工介入问题。解决了性能问题,但是业务却变得超复杂
      • 适用于 减库存 业务
    • 异步确保:利用MQ实现;发送方发送完一条消息后就不管了,而接收消息那一方必须保证这个消息能处理成功,因为只要把消息发送出去不丢失,那就可以一直进行重试,直到重试成功为止

      • 事务无法回滚,不合适减库存业务
      • 关键:消息是否丢失
      • 适用于 转账 业务,只要消息可靠不丢失就行
    • 2PC+MQ:异步确保结合两阶段提交

    总结在电商行业中比较适合的是TCC,虽然业务变得复杂了,但是可靠;根据业务不同使用不同的分布式事务。


    但是在我们这个小项目中,业务没有那么复杂(没有优惠券与积分等功能),在这里我们采用同步调用的方式解决。

    采用同步调用
    • 先查询库存,然后if判断库存,足够多时就进行减库存操作

      逻辑是对的,但是这么做有线程上的安全问题,当线程很多、高并发时,有可能引发超卖问题

    • 加锁 - synchronized

      • 安全,但是性能太差,只有一个线程可以执行,当搭了集群,同时启动好几台服务时,synchronized只锁住了当前一个tomcat,锁不了别的。
      • 看起来是安全的,但是在分布式服务下不安全(原因:假如商品微服务启动三台,也就是三个不同的JVM,锁的原理是内存的一种监视,三个JVM内存就不一致,则三个减库存方法的锁就不一致,只能一个JVM内部锁,不能锁住对方,因此线程不安全,放在Redis只是数据一致,不代表对象一致,锁不是一致的,JVM的范围只能作用它自己,所以不能利用synchronized锁来实现 )
    • 分布式锁:专门来锁分布式服务的

      • zookeeper
        zookeeper是树(目录)结构,利用节点的唯一性来实现,也就意味如果有人连到zookeeper以后,会在zookeeper上创建一个树节点(在目录下创建一个节点),一旦有人创建出来,别人就不能再创建同名节点。任何一个代码进入到减库存这个地方,先去通过zookeeper在某个目录下创建一个节点,创建成功就认为得到了锁,继续执行代码;反之则失败(别人已经创建过了),返回或者wait(取决表如何设计:是否自旋),因此只有一个人可以拿到这个锁,执行完毕后删除节点释放锁,其他人可以再次创建锁 ,这样就实现单线程。—靠的是外部工具来模拟锁的机制(权限,同一时刻只能一个人操作)
      • Redis:SETNX命令
        同zookeeper原理类似,但SETNX命令只能set不存在的key,如果不存在,创建并设置value,并返回1;如果存在则会set失败,并返回0,代码运行完以后可以掉用del命令释放锁
        • 但推荐zookeeper分布式锁,而不是Redis。原因:Redis分布式锁存在搜索问题,如果SETNX成功,成功之后开始执行代码,但是此时服务器宕机,那del命令就一直没有执行,等于这个锁一直被拿着,无法释放,以后这个值将无法再被set成功,因此我们得想办法解决死锁问题在,服务器宕机的情况下想办法剔除这个值
        • 而zookeeper可以创建临时节点,当服务器一旦断开与zookeeper连接,会自动删除该节点,自动释放锁,但逻辑实现起来较复杂。

    但是我们不推荐用锁实现,因为用了锁,就变成单线程了,相当于一执行减库存操作就先把数据库锁死,同一时刻只能有一个人来操作(类似于悲观锁,即认为线程安全问题一定会发生),在面对高并发时,往往性能很差


    既然不推荐悲观锁,那是不是可以采用乐观锁呢?乐观锁认为线程安全问题不会发生,不加锁,但是不加锁会有线程安全问题,那如何处理这件事情呢?


    答:我们不做查询也不做判断,而是直接去进行减库存操作(上边说了,直接进行减库存会发生超卖现象,不过这要看我们sql如何写了)我们可以在sql内部加上条件进行判断,任何人来执行,它都会有一个条件来判断库存,如果库存不足sql本身就会失败,事务回滚,因此不需要加锁与判断本质上还是乐观锁,不需要等待、阻塞,任何人都可以执行,如果执行失败会反馈失败信息,而不像是悲观锁那样线程阻塞,导致一直等待,而且还会锁数据库与表(只有执行完成才会释放),性能上优于加锁方式,语句如下:

     "UPDATE tb_stock SET stock = stock - #{num} WHERE sku_id = #{id} AND stock >= #{num}"
    
    展开全文
  • PHP解决高并发问题

    万次阅读 多人点赞 2020-05-27 12:10:04
    我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的并发场景,这个指标非常关键。举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台...

    举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定出现大塞车。(5条车道忽然变成4条车道的感觉)

    同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。

    14834077821.jpg

    其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数占满,其他正常的业务请求,无连接进程可用。

    更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。

    重启与过载保护

    如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。

    秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回

    高并发下的数据安全

    我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

    1. 超发的原因

    假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。(同文章前面说的场景)

    14834077822.jpg

    在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

    优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false

     <?php
     //优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false
     include('./mysql.php');
     $username = 'wang'.rand(0,1000);
     //生成唯一订单
     function build_order_no(){
       return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
     }
     //记录日志
     function insertLog($event,$type=0,$username){
         global $conn;
         $sql="insert into ih_log(event,type,usernma)
         values('$event','$type','$username')";
         return mysqli_query($conn,$sql);
     }
     function insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number)
     {
           global $conn;
           $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price,username,number)
           values('$order_sn','$user_id','$goods_id','$sku_id','$price','$username','$number')";
          return  mysqli_query($conn,$sql);
     }
     //模拟下单操作
     //库存是否大于0
     $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' ";
     $rs=mysqli_query($conn,$sql);
     $row = $rs->fetch_assoc();
       if($row['number']>0){//高并发下会导致超卖
           if($row['number']<$number){
             return insertLog('库存不够',3,$username);
           }
           $order_sn=build_order_no();
           //库存减少
           $sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0";
           $store_rs=mysqli_query($conn,$sql);
           if($store_rs){
               //生成订单
               insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number);
               insertLog('库存减少成功',1,$username);
           }else{
               insertLog('库存减少失败',2,$username);
           }
       }else{
           insertLog('库存不够',3,$username);
       }
     ?>
    
    1. 悲观锁思路

    解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。

    悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

    14834077833.jpg

    虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

    优化方案2:使用MySQL的事务,锁住操作的行

     <?php
     //优化方案2:使用MySQL的事务,锁住操作的行
     include('./mysql.php');
     //生成唯一订单号
     function build_order_no(){
       return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
     }
     //记录日志
     function insertLog($event,$type=0){
         global $conn;
         $sql="insert into ih_log(event,type)
         values('$event','$type')";
         mysqli_query($conn,$sql);
     }
     //模拟下单操作
     //库存是否大于0
     mysqli_query($conn,"BEGIN");  //开始事务
     $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行
     $rs=mysqli_query($conn,$sql);
     $row=$rs->fetch_assoc();
     if($row['number']>0){
         //生成订单
         $order_sn=build_order_no();
         $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
         values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
         $order_rs=mysqli_query($conn,$sql);
         //库存减少
         $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
         $store_rs=mysqli_query($conn,$sql);
         if($store_rs){
           echo '库存减少成功';
             insertLog('库存减少成功');
             mysqli_query($conn,"COMMIT");//事务提交即解锁
         }else{
           echo '库存减少失败';
             insertLog('库存减少失败');
         }
     }else{
       echo '库存不够';
         insertLog('库存不够');
         mysqli_query($conn,"ROLLBACK");
     }
     ?>
    
    1. FIFO队列思路

    那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。

    14834077834.jpg

    然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。

    1. 文件锁的思路
      对于日IP不高或者说并发数不是很大的应用,一般不用考虑这些!用一般的文件操作方法完全没有问题。但如果并发高,在我们对文件进行读写操作时,很有可能多个进程对进一文件进行操作,如果这时不对文件的访问进行相应的独占,就容易造成数据丢失

    优化方案4:使用非阻塞的文件排他锁

     <?php
     //优化方案4:使用非阻塞的文件排他锁
     include ('./mysql.php');
     //生成唯一订单号
     function build_order_no(){
       return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
     }
     //记录日志
     function insertLog($event,$type=0){
         global $conn;
         $sql="insert into ih_log(event,type)
         values('$event','$type')";
         mysqli_query($conn,$sql);
     }
     $fp = fopen("lock.txt", "w+");
     if(!flock($fp,LOCK_EX | LOCK_NB)){
         echo "系统繁忙,请稍后再试";
         return;
     }
     //下单
     $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
     $rs =  mysqli_query($conn,$sql);
     $row = $rs->fetch_assoc();
     if($row['number']>0){//库存是否大于0
         //模拟下单操作
         $order_sn=build_order_no();
         $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
         values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
         $order_rs =  mysqli_query($conn,$sql);
         //库存减少
         $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
         $store_rs =  mysqli_query($conn,$sql);
         if($store_rs){
           echo '库存减少成功';
             insertLog('库存减少成功');
             flock($fp,LOCK_UN);//释放锁
         }else{
           echo '库存减少失败';
             insertLog('库存减少失败');
         }
     }else{
       echo '库存不够';
         insertLog('库存不够');
     }
     fclose($fp);
      ?>
    
     <?php
     //优化方案4:使用非阻塞的文件排他锁
     include ('./mysql.php');
     //生成唯一订单号
     function build_order_no(){
       return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
     }
     //记录日志
     function insertLog($event,$type=0){
         global $conn;
         $sql="insert into ih_log(event,type)
         values('$event','$type')";
         mysqli_query($conn,$sql);
     }
     $fp = fopen("lock.txt", "w+");
     if(!flock($fp,LOCK_EX | LOCK_NB)){
         echo "系统繁忙,请稍后再试";
         return;
     }
     //下单
     $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
     $rs =  mysqli_query($conn,$sql);
     $row = $rs->fetch_assoc();
     if($row['number']>0){//库存是否大于0
         //模拟下单操作
         $order_sn=build_order_no();
         $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
         values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
         $order_rs =  mysqli_query($conn,$sql);
         //库存减少
         $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
         $store_rs =  mysqli_query($conn,$sql);
         if($store_rs){
           echo '库存减少成功';
             insertLog('库存减少成功');
             flock($fp,LOCK_UN);//释放锁
         }else{
           echo '库存减少失败';
             insertLog('库存减少失败');
         }
     }else{
       echo '库存不够';
         insertLog('库存不够');
     }
     fclose($fp);
      ?>
    
    1. 乐观锁思路

    这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。

    在这里插入图片描述
    有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

    优化方案5:Redis中的watch

     <?php
     $redis = new redis();
      $result = $redis->connect('127.0.0.1', 6379);
      echo $mywatchkey = $redis->get("mywatchkey");
     /*
       //插入抢购数据
      if($mywatchkey>0)
      {
          $redis->watch("mywatchkey");
       //启动一个新的事务。
         $redis->multi();
        $redis->set("mywatchkey",$mywatchkey-1);
        $result = $redis->exec();
        if($result) {
           $redis->hSet("watchkeylist","user_".mt_rand(1,99999),time());
           $watchkeylist = $redis->hGetAll("watchkeylist");
             echo "抢购成功!<br/>";
             $re = $mywatchkey - 1;  
             echo "剩余数量:".$re."<br/>";
             echo "用户列表:<pre>";
             print_r($watchkeylist);
        }else{
           echo "手气不好,再抢购!";exit;
        } 
      }else{
          // $redis->hSet("watchkeylist","user_".mt_rand(1,99999),"12");
          //  $watchkeylist = $redis->hGetAll("watchkeylist");
             echo "fail!<br/>";   
             echo ".no result<br/>";
             echo "用户列表:<pre>";
           //  var_dump($watchkeylist); 
      }*/
     $rob_total = 100;   //抢购数量
     if($mywatchkey<=$rob_total){
         $redis->watch("mywatchkey");
         $redis->multi(); //在当前连接上启动一个新的事务。
         //插入抢购数据
         $redis->set("mywatchkey",$mywatchkey+1);
         $rob_result = $redis->exec();
         if($rob_result){
              $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);
             $mywatchlist = $redis->hGetAll("watchkeylist");
             echo "抢购成功!<br/>";
           
             echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>";
             echo "用户列表:<pre>";
             var_dump($mywatchlist);
         }else{
               $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao');
             echo "手气不好,再抢购!";exit;
         }
     }
     ?>
    
    展开全文
  • Redis解决库存超卖问题

    千次阅读 2021-01-26 15:59:41
    在订单生成时直接扣库存,这是最原始的扣库存方案,比较简单,但存在一系列问题: 可能导致很多订单把产品库存扣除而未支付,这就需要有一个后台脚本,将一段时间内没有支付的订单的库存释放,把订单取消 即时扣...

    商品和订单服务间使用MQ

    商品服务的库存变化时,通过 MQ 通知订单服务库存变化。

    原始的同步流程

    1. 查询商品信息 (调用商品服务)
    2. 计算总价(生成订单详情)
    3. 商品服务扣库存(调用商品服务)
    4. 订单入库( 生成订单)
    // 原始的MySQL同步流程
    // 判断此代金券是否加入抢购
    SeckillVouchers seckillVouchers = seckillVouchersMapper.selectVoucher(voucherId);
    AssertUtil.isTrue(seckillVouchers == null, "该代金券并未有抢购活动");
    // 判断是否有效
    AssertUtil.isTrue(seckillVouchers.getIsValid() == 0, "该活动已结束");
    // 插入数据库
    seckillVouchersMapper.save(seckillVouchers);
    

    在订单生成时直接扣库存,这是最原始的扣库存方案,比较简单,但存在

    问题

    • 可能导致很多订单把产品库存扣除而未支付,这就需要有一个后台脚本,将一段时间内没有支付的订单的库存释放,把订单取消
    • 即时扣库存,并发差

    1、3步商品服务,操作商品服务的 db,2、4步订单服务,操作订单服务的 db。

    避免访问不同服务的 db,原则上同一服务只能操作自身服务的 db。

    MQ异步化

    首先考虑只将第4步异步。

    分析

    2,4都是操作db,第4步不再等待,1、2、3成功后立即反馈给用户。

    之后通过消息通知服务异步下单,若第4步异步下单失败,重试操作,试图重新生成订单,MQ的消息也可回溯。

    订单创建完成后,处于排队状态,然后服务发布一个事件Order Created 到消息队列中。
    即订单服务向外界发送消息:我创建了一个订单,由MQ 转发给订阅该消息的服务。

    如果商品服务收到创建订单消息之后执行扣库存操作。注意,这里可能因为某些不可抗因素导致扣库存失败,无论成功与否,商品服务都会发送一个扣库存消息到 MQ,消息内容即扣库存的结果。
    订单服务会订阅扣库存的结果,接收到该消息后:

    • 如果扣库存成功,将订单的状态改为已确认,即下单成功
    • 如果扣库存失败,将订单的状态改为已取消,即下单失败

    欲实现上述模型要求,需可靠的消息投递。服务发出的消息,一定会被MQ收到。

    • 用户体验的变化
      前端配合排队中等界面。

    商品/订单服务都变成异步化,适合秒杀类场景,当流量不大时,并不太适合。

    异步设计

    1. 库存在Redis中保存
    2. 收到请求Redis判断是否库存充足 ,减掉Redis中库存
    3. 订单服务创建订单写入数据库,并发送消息

    当订单支付成功后,会有一个出库过程,既然有这个过程,就有可能出库失败。
    库存有两部分:

    • 缓存redis层
    • 数据库mysql层
    1. 当客服新增5个库存,则缓存redis和数据库mysql层都需增加5个库存,使用分布式事务的最终一致性来满足:库存要么全加,要么全不加。
    2. 当订单生成时,需要扣除库存,先扣redis库存,如果扣除成功,则生成订单进行支付,这个过程不扣除mysql库存
    3. 当redis库存扣完,该产品就无法下单了,下单就会失败,就把外层的给挡住了。
    4. 在第2步扣除redis库存成功后,生成订单,进行支付,支付成功,返回我的订单中心, 会发现有一个出库过程。
    5. 出库过程
      一个MQ异步解耦的任务队列,这个过程是扣除mysql库存
    • 如果扣mysql库存成功,出库成功,完成下订单整个流程,进入发货状态
    • 如果扣mysql库存失败,出库失败,进行一系列的操作
      • 订单状态改成取消
      • 返还redis库存
      • 退款

    redis库存和mysql库存

    支付前是预扣,是扣redis库存,是锁定库存的过程
    支付后是真正扣,扣mysql库存,保证库存最终一致

    但是,在极端情况下会存在数据不一致

    • 如果redis库存 = mysql库存,不会有问题
    • 如果redis库存 < mysql库存,不会有超卖问题,但会存在实际有库存,但是没有卖的情况
    • 如果redis库存 > mysql库存,就会超卖,超卖的订单,在出库的过程中会失败

    这样总体不会出问题,mysql数据库层,保证库存最终不会出问题。

    问题

    数据库库存和redis库存不一致,如何检测?

    如果检测出来不一致,如何同步

    没有想出来好的方案
    比较暴力的方式,就是找一个低峰期,譬如凌晨1点,周期性强行覆盖。 但是极端情况下还是会存在同步后不准确,譬如在同步的过程中,刚好有一个订单在支付,这个订单支付成功后,出库的过程中,扣除了mysql的库存,但是没有扣除redis的库存

    这个就是数据库同步缓存的更新机制方面的问题
    属于一致性的逻辑设计的问题
    缓存数 = 数据库库存数 - 待扣数
    当然这里面也还有其它的方案,以及考虑到一致性的要求高低,可以使用简单或复杂的方案
    就看系统复杂度了,越是大系统就要拆得越细
    比如待扣数又可以放到一个队列里面,或者缓存里面,同时有计数,直接读计数就行
    比如放到mongo,已支付待出库的数量,一般也不会很大,count一下,也不会损失多少
    所以一般系统都不能完全保障数据链不出错,但一定要有补偿,就是出错了可以纠错
    要保障不出错的代价显然太大
    同步是有一套刷新机制,可以定时,也可以通过MQ,或者监控不一至同步等等。。。
    也叫做保障缓存数据的新鲜度
    一般不会太长时间,半小时,几分钟都有可能,不同场景需求不一样

    12306

    买火车票的12306,晚上的时间都不能买票,这个时间估计是在同步库存,将数据库库存同步到redis库存中, 但是买火车票之类,在订单生成前,必须扣除实际库存,也就是要扣除mysql的库存,

    因为买火车票和购物不一样,购物可以付款后出库,但是买票这种,支付前就必须出库,因此,要将出库过程提前, 只有出库成功,才能生成订单,同样要引入redis库存

    先扣缓存中的库存,扣除成功后,然后才可以去扣mysql中的库存。

    如果扣除缓存中的库存失败,就会挡在外面,返回库存不足,这些请求不会穿刺到mysql中,挡住了大多数的请求压力。

    redis库存会和mysql库存不一致,极端情况下是肯定有的,需要进行库存同步

    • 当缓存库存比数据库库存多,那么就会出现,查询有票,但是就无法下单,下单的时候就说库存不足,这个情况下,就会造成数据库压力过大,不过12306应该有其他手段来规避这个问题,不过,我确实遇到过,查询的时候有票,但是无法下单的情况。

    • 当缓存库存比数据库缓存少,那么不会出问题,只会出现有票,但是没有出售的情况,等完成库存同步一下, 明天又准确了。

    展开全文
  • 前言:并发的秒杀活动中,通过查询数据库判断是否还有库存,然后对库存字段进行增减,极易出现库存超出或者库存为负的情况,一般来说有3中解决办法(数据库表加锁,memche缓存,redis队列);我们这里使用redis来...
  • 如何正解决库存超卖问题

    千次阅读 2018-08-29 21:44:29
    然而,作为活动商品,库存肯定是很有限的,如何控制库存不让出现超买,以防止造成不必要的损失是众多电子商务网站程序员头疼的问题,这同时也是最基本的问题。 [内容]  注意!文中说到缓存用memcache,并以此作...
  • python 中如何解决高并发问题

    千次阅读 2019-01-23 16:34:44
    python 中并发问题解决 描述:在多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。 例如:id为16的商品的库存为10,两人同时购买...
  • Redis Lua脚本解决秒杀下库存校验问题场景基本需求涉及问题解决思路 场景 基本需求 秒杀活动,到时间点后,用户会对商品进行购买。 涉及问题 秒杀场景下,瞬时的并发会比较 商品的数量是有限的,不能超买超卖 每...
  • javaEE并发之如何更新库存问题

    千次阅读 2018-11-26 12:32:38
    javaEE并发之如何更新库存问题  有三个阶段可更新库存:成功加入购物车;点击去支付,生成订订单;点击支付。 分析: 1、加入购物车并不代表用户一定会购买,如果这个时候开始预占库存,会导致想购买的无法加入...
  • 并发下防止库存超卖解决方案

    千次阅读 2020-05-28 12:16:28
    目前网上关于防止库存超卖,我没找到可以支持一次购买多件的,都是基于一次只能购买一件做的秒杀方案,但是实际场景中,一般秒杀活动都是支持1~5件的,因此为了补缺,写了此文,方便自己之后使用。   二、建表 ...
  • 关于热销商口,库存问题如何解决
  • MySQL处理并发,防止库存超卖的问题,在去年的时候,王总已经提过;但是很可惜,即使当时大家都听懂了,但是在现实开发中,还是没这方面的意识。今天就我的一些理解,整理一下这个问题,并希望以后这样的课程能...
  • 分布式锁(解决库存超买超卖的问题)

    千次阅读 2020-03-12 15:02:25
    解决库存超买超卖的问题 单个应用解决线程安全问题可以加synchronized 关键字 : 其实就是一个标记,谁拿到标记才可以执行方法,才有cpu的运行资格. 但是分布式的情况下 synchronized 关键字锁不住. 所以需要使用分布式...
  • 问题描述: 某电商平台,首发一款新品手机,每人限购2台,预计会有10W的并发,在该情况下,如果扣减库存,保证不会超卖 解决方案一 利用数据库锁机制,对记录进行锁定,再进行操作 SELECT * from goods where ID =1 ...
  • 这个问题和秒杀,库存问题是类似的. https://yq.aliyun.com/edu/lesson/play/270 LongAdder fei33423 java分段并发计数器,用于统计计算分析. 【公开课】【阿里在线技术峰会】何登成:AliSQL性能优化与功能突破...
  • 并发下防止库存超卖的解决方案

    千次阅读 2020-02-29 20:56:41
    最近在看秒杀相关的项目,针对防止库存超卖的问题,查阅了很多资料,其解决方案可以分为悲观锁、乐观锁、分布式锁、Redis原子操作、队列串行化等等,这里进行浅显的记录总结。 首先我们来看下库存超卖问题是怎样...
  • Java解决高并发下商品库存更新

    千次阅读 2019-01-02 13:33:00
    然而,作为活动商品,库存肯定是很有限的,如何控制库存不让出现超买,以防止造成不必要的损失是众多电子商务网站程序员头疼的问题,这同时也是最基本的问题。 从技术方面剖析,很多人肯定会想到事务,但是事务是...
  • 问题描述: 库存更新成负数 产生原因: 由于多线程并发时每个下单线程判断是否超库存时,读到了数据库同样的值,都认为库存满足要求,都执行了下单扣库存的操作,结果就是库存被更新成了负数,实际下单量大于实际...
  • 1. 大型并发系统架构 2.秒杀抢购系统选型 3. 扣库存的艺术 4. 代码演示 5.总结回顾 12306抢票,极限并发带来的思考? 每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票!虽然现在...
  • php解决高并发问题

    万次阅读 2019-02-20 16:12:33
    我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的并发场景,这个指标非常关键。举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台...
  • 乐观锁和悲观锁看名称挺高大上的,面试的时候一些面试官最喜欢拿这个来考应聘者。...可以有效防止减库存冲突问题。 什么是乐观锁?相反就是认为没几个人用,基本不会碰到有人修改,所以查询时就不加锁,然后更新的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 52,691
精华内容 21,076
关键字:

如何解决高库存问题