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)悲观锁和乐观锁的使用场景

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

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

原文地址:https://www.cnblogs.com/maoxinjueluo/p/12883162.html