• MySQL事务


    MySQL事务的介绍

    1) 事务概念

    一组mysql语句,要么执行,要么全不不执行。

     

    2) 事务的特点

    1原子性:一组事务,要么成功;要么撤回。

    2稳定性 有非法数据(外键约束之类),事务撤回。

    3隔离性事务独立运行。一个事务处理后的结果,影响了其他事务,那么其他事务会撤回。事务的100%隔离,需要牺牲速度。

    4可靠性:软、硬件崩溃后,InnoDB数据表驱动会利用日志文件重构修改。可靠性和高速度不可兼得, innodb_flush_log_at_trx_commit 选项 决定什么时候吧事务保存到日志里。

    3) 事务控制语句

    • 开启事务:BEGINSTART TRANSACTION;显式地开启一个事务;

    • 提交事务:COMMIT;也可以使用COMMIT WORK,不过二者是等价的。COMMIT会提交事务,并使已对数据库进行的所有修改称为永久性的;

    • 回滚:ROLLBACK;有可以使用ROLLBACK WORK,不过二者是等价的。回滚会结束用户的务,并撤销正在进行的所有未提交的修改;

    • 保存节点:SAVEPOINT identifierSAVEPOINT允许在事务中创建一个保存点,一个事务中可以有多个SAVEPOINT

    • 删除事务:RELEASE SAVEPOINT identifier;删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;

    • 回滚到保存的节点:ROLLBACK TO identifier;把事务回滚到标记点;

    4) mysql事务隔离级别

    SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

    • (1)Read Uncommitted(读取未提交内容)

      在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

    • (2)Read Committed(读取提交内容)

      这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

    • (3)Repeatable Read(可重读)

           这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的幻影行。InnoDBFalcon存储引擎通过多版本并发控制(MVCCMultiversion Concurrency Control)机制解决了该问题。

    • (4)Serializable(可串行化)       

      这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

    这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:

    • 脏读(Drity Read)某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
    • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。

    • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几行(Row)数据,而另一个事务却在此时插入了新的几数据,先前的事务在接下来的查询中,就会发现有几数据是它先前所没有的。

    5) 设置mysql事务的隔离级别

    打开mysql配置文件: sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf, 添加如下行。

     

    保存配置文件,重启mysql服务。

    sudo service mysql restart

    django中使用事务

    下面以天天生鲜项目中的订单提交为例,说明django中事务的使用:

     1 from django.db import transaction
     2 class OrderCommitView(View):
     3     '''订单创建'''
     4 
     5     @transaction.atomic  ##  使用事务
     6     def post(self,request):
     7         '''订单创建'''
     8         ##  判断用户是否登录
     9         user = request.user
    10         if not user:
    11             return JsonResponse({'res':0,'errmsg':'用户未登录'})
    12 
    13         ##  1、接收数据
    14         pay_method = request.POST.get('pay_method')
    15         addr_id = request.POST.get('addr_id')
    16         skus_id = request.POST.get('skus_id')
    17         ##  将skus_id的格式转化成列表形式
    18         skus_id = skus_id.split(',')
    19 
    20 
    21         ##  2、校验数据
    22         if not all([pay_method,addr_id,skus_id]):
    23             return JsonResponse({'res':1,'errmsg':'数据不完整'})
    24         skus = []
    25         for sku_id in skus_id:
    26             ##  遍历skus_id,从数据库中查询sku
    27             try:
    28                 sku = GoodsSKU.objects.get(id=sku_id)
    29                 skus.append(sku)
    30             except GoodsSKU.DoesNoExist:
    31                 return JsonResponse({'res':2,'errmsg':'商品不存在'})
    32         try:
    33             addr = Address.objects.get(id=addr_id)
    34         except Address.DoesNoExist:
    35             return JsonResponse({'res':3,'errmsg':'地址不存在'})
    36         ##  校验支付方式
    37         if str(pay_method) not in OrderInfo.PAY_METHODS.keys():
    38             return JsonResponse({'res':4,'errmsg':'错误的支付方式'})
    39         ##  设置事务的节点
    40         save_id = transaction.savepoint()
    41 
    42         ##  3、业务处理
    43         ##########  (1)先向订单数据库中添加一条数据,准备数据:order_id、user、addr、pay_method、total_count、total_price、transit_price
    44         ##  计算total_count和total_price,先初始化为0,后面查询各个商品的数量和价格之后再进行累加,以及重新保存
    45         try:
    46             total_count = 0
    47             total_price = 0
    48             ##  生成order_id:格式:当前的时间+sku_id
    49             order_id = datetime.now().strftime('%Y%m%d%H%M%S') + str(user.id)
    50             ##  添加数据,创建新的订单数据
    51             order = OrderInfo.objects.create(order_id=order_id,
    52                                      user=user,
    53                                      addr=addr,
    54                                      pay_method=pay_method,
    55                                      total_count=total_count,
    56                                      total_price=total_price,
    57                                      transit_price=10)
    58     
    59             ##########  (2)向商品订单中添加多条数据,准备数据:order、sku、count、price
    60             conn = get_redis_connection('default')
    61             cart_key = 'cart_%d'%user.id
    62             for sku in skus:
    63                 count = conn.hget(cart_key,sku.id)
    64                 price = sku.price
    65                 OrderGoods.objects.create(order=order,
    66                                           sku=sku,
    67                                           count=count,
    68                                           price=price)
    69                 if sku.stock < int(count):
    70                     ##  业务失败时,回滚事务
    71                     transaction.rollback(save_id)
    72                     return JsonResponse({'res':6,'errmsg':'库存不足'})
    73                 ##  MySQL数据库的库存减少,销量增加
    74                 sku.stock -= int(count)
    75                 sku.sales += int(count)
    76                 sku.save()
    77                 ##  删除Redis数据库中添加到订单中的数据
    78                 conn.hdel(cart_key,sku.id)
    79                 ##  累加total_count 和total_price
    80                 total_count += int(count)
    81                 total_price += int(count)*int(price)
    82             ##  重新更新订单中的total_count和total_price
    83             order.total_count = total_count
    84             order.total_price = total_price
    85             order.save()
    86         
    87         except Exception:
    88             ##  业务失败时,回滚事务
    89             transaction.rollback(save_id)
    90             return JsonResponse({'res':7,'errmsg':'订单创建失败'})
    91 
    92         ##  4、返回应答
    93         return JsonResponse({'res':5,'message':'创建订单成功'})
    django中使用事务

    参考文档:数据库事务

    订单并发处理

    • 1、订单并发问题

    假设数据库中鸡腿的库存只有1,这个时候有两个用户同时提交订单,就会出现下面的情况:

       一个用户使用一个进程,当用户1执行到2的时候,这时判断库存是1,刚好切换到进程2,所以用户2顺利下单,再切换到用户1 的时候,用户1用于之前判断库存为1,所以也能够顺利下单,这就造成了只有1个库存,却下了两个订单,显然这是不允许的,所以就必须要给进程加锁,下面是两种解决方案:悲观锁和乐观锁

    • 2、订单并发解决

    (1)悲观锁

    悲观锁的原理:在查询数据库时加锁,当用户拿到锁时,其它用户不能对数据库进行操作,直到该用户对数据库操作结束时才释放锁。

    加锁的语句:

    select * from df_goods_info where id=sku_id for update;

    django中查询数据库加锁:

    sku = GoodsSKU.objects.select_for_update().get(id=sku_id)

     

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

    (2)乐观锁

    乐观锁原理:查询时不锁数据,提交更改时进行判断

    加锁的语句:

    update df_goods_sku set stock=0, sales=1 where id=sku_id and stock=1;

     django更新数据库时加锁的语句:

    res = GoodsSKU.objects.filter(id=sku_id, stock=orgin_stock).update(stock=new_stock, sales=new_sales)

    这里注意一下:

      乐观锁是判断更新完的数据是否与查询时的数据一样,如果一样就返回1,否则返回0。这里就存在一个问题:有两个用户同时下单,假如库存为3,当两个用户查询结束时,此时两个用户查询到的库存都是3,其中用户A拿到乐观锁,进行数据库的修改,将数据库的库存改为2,用户B拿到乐观锁后,由于此时库存与用户B之前查到的数据不一致,所以用户B下单失败。

    解决方法:

      通常我们会给用户3次机会进行数据库修改,所以可以将查询语句以及修改语句放在for循环里面。并且MySQL默认是 Repeatable Read(可重读),也就是用户A更新完的数据用户B是拿不到的,所以需要将MySQL事务隔离级别改为Read Committed(读取提交内容)

     

    具体实例:

    class OrderCommitView(View):
        '''订单创建'''
        @transaction.atomic
        def post(self, request):
            '''订单创建'''
           # 设置事务保存点
            save_id = transaction.savepoint()
            try:for sku_id in sku_ids:
                    for i in range(3):
                        # 获取商品的信息
                        try:
                            sku = GoodsSKU.objects.get(id=sku_id)
                        except:
                            # 商品不存在
                            transaction.savepoint_rollback(save_id)
                            return JsonResponse({'res':4, 'errmsg':'商品不存在'})
    
                        # 从redis中获取用户所要购买的商品的数量
                        count = conn.hget(cart_key, sku_id)
    
                        # todo: 判断商品的库存
                        if int(count) > sku.stock:
                            transaction.savepoint_rollback(save_id)
                            return JsonResponse({'res':6, 'errmsg':'商品库存不足'})
    
                        # todo: 更新商品的库存和销量
                        orgin_stock = sku.stock
                        new_stock = orgin_stock - int(count)
                        new_sales = sku.sales + int(count)
    
                        # 返回受影响的行数
                        res = GoodsSKU.objects.filter(id=sku_id, stock=orgin_stock).update(stock=new_stock, sales=new_sales)
                        if res == 0:
                            if i == 2:
                                # 尝试的第3次
                                transaction.savepoint_rollback(save_id)
                                return JsonResponse({'res': 7, 'errmsg': '下单失败2'})
                            continue
    
                        # todo: 向df_order_goods表中添加一条记录
                        OrderGoods.objects.create(order=order,
                                                  sku=sku,
                                                  count=count,
                                                  price=sku.price)
    
                        # todo: 累加计算订单商品的总数量和总价格
                        amount = sku.price*int(count)
                        total_count += int(count)
                        total_price += amount
    
                        # 跳出循环
                        break
    
                # todo: 更新订单信息表中的商品的总数量和总价格
                order.total_count = total_count
                order.total_price = total_price
                order.save()
            except Exception as e:
                transaction.savepoint_rollback(save_id)
                return JsonResponse({'res':7, 'errmsg':'下单失败'})
    
            # 提交事务
            transaction.savepoint_commit(save_id)
    # 返回应答
            return JsonResponse({'res':5, 'message':'创建成功'})

     (3)悲观锁和乐观锁的使用场景

    • 冲突比较少的时候,使用乐观锁。

    • 冲突比较多的时候,使用悲观锁。

  • 相关阅读:
    第五周:函数
    第四周:循环控制
    第三周:循环
    第二周:判断
    第一周:简单的计算程序
    cocos2dx-lua采用pbc解析proto问题修正
    cocos2dx 3.9.1 mac模拟器log输出bug修正
    基于dragonbones的cocos2dx lua封装
    EF 3.0 要开启 数据库连接复用 MultipleActiveResultSets=true
    找不到资产文件“xxxxobjproject.assets.json
  • 原文地址:https://www.cnblogs.com/maoxinjueluo/p/12883162.html
Copyright © 2020-2023  润新知