悲观锁乐观锁在django中使用

1 悲观锁乐观锁
1.1 并发控制
1.1 悲观锁
1.2 乐观锁
1.3 悲观锁乐观锁使用场景

2 django中开启事务
2.1 全局开启事务
2.2 视图开启事务
2.3 局部使用事务
2.4 savepoint回滚
2.5 事务提交后回调函数

3 django中使用悲观锁
3.1 前置条件,表模型
3.1 模拟秒杀生成订单场景

4 django使用乐观锁
4.1 普通版
4.2 升级版

1 悲观锁乐观锁

无论是悲观锁还是乐观锁,都是人们定义出来的概念,仅仅是一种思想,与语言无关

1.1 并发控制

1 并发控制:当程序中出现并发问题时,就需要保证在并发情况下数据的准确性,以此确保当前用户
	和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的,这种手段就叫做并发控制
2 没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题

1.1 悲观锁

# 悲观锁是什么?
1 悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,
	最好的办法就是直接对该数据进行加锁以防止并发。
2 这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制
	【Pessimistic Concurrency Control,缩写"PCC",又名"悲观锁"3 之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。
	总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,
	因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。


# 悲观锁的实现方式:
1 传统的关系型数据库(如mysql)使用这种锁机制,
	比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
2 编程语言中的线程锁,比如python的互斥锁
3 分布式锁:redis实现的分布式锁等
​

1.2 乐观锁

# 乐观锁是什么?
1 通过程序实现,不会产生死锁
2 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,
	只在更新的时候会判断一下在此期间别人有没有去更新这个数据
​
# 乐观锁实现方式
1 CAS(Compare And Swap) 即比较并交换。
	是解决多线程并行情况下使用锁造成性能损耗的一种机制,
	CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B),执行CAS操作的时候,
	将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,
	否则,处理器不做任何操作
  
   CAS会出现ABA问题,比如,你看到桌子上有100块钱,然后你去干其他事了,回来之后看到桌子上
   依然是100块钱,你就认为这100块没人动过,其实在你走的那段时间,别人已经拿走了100块,
   后来又还回来了。这就是ABA问题
  
  解决ABA问题:
  	既然有人动了,那我们对数据加一个版本控制字段,只要有人动过这个数据,就把版本进行增加,
  	我们看到桌子上有100块钱版本是1,回来后发现桌子上100没变,但是版本却是2,
  	就立马明白100块有人动过
​
2 版本号控制:
	在数据表中增加一个版本号字段,每次更新数据时将版本号加1,同时将当前版本号作为更新条件,
	如果当前版本号与更新时的版本号一致,则更新成功,否则更新失败
3 时间戳方式:
	在数据表中增加一个时间戳字段,每次更新数据时将时间戳更新为当前时间戳,
	同时将当前时间戳作为更新条件,如果当前时间戳与更新时的时间戳一致,则更新成功,否则更新失败

1.3 悲观锁乐观锁使用场景

并发量:如果并发量不大,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,
	悲观锁定会带来非常大的性能问题, 建议乐观锁

响应速度:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,
	不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高
	
冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率。
	冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大
	
重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁,效率低。更新失败的概率比较低

读多写少: 乐观锁适用于读多写少的应用场景,这样可以提高并发粒度

2 django中开启事务

Django是支持事务操作的,它的默认事务行为是自动提交,

具体表现形式为:每次数据库操作(比如调用save()方法)会立即被提交到数据库中。

但是如果你希望把连续的SQL操作包裹在一个事务里,就需要手动开启事务。

2.1 全局开启事务

DATABASES = {
     'default': {
         'ENGINE': 'django.db.backends.mysql',
         'NAME': 'lqz',
         'HOST': '127.0.0.1',
         'PORT': '3306',
         'USER': 'lqz',
         'PASSWORD': 'lqz123',
          #全局开启事务,绑定的是http请求响应整个过程
         'ATOMIC_REQUESTS': True, 
     }
}
​

1 每当有请求过来时,Django会在调用视图方法前开启一个事务。
	如果完成了请求处理并正确返回了结果,Django就会提交该事务。否则,Django会回滚该事务。
​
2 虽然全局开启事务很简单,但Django并不推荐开启全局事务。因为一旦将事务跟 HTTP 请求绑定到
	一起时,每一个请求都会开启事务,当访问量增长到一定的时候会造成很大的性能损耗。
	在实际开发过程中,很多GET请求根本不涉及到事务操作,一个更好的方式是局部开启事务按需使用
​

局部禁用全局事务

from django.db import transaction
# 局部禁用事务
@transaction.non_atomic_requests
def seckill(request):
    return HttpResponse('秒杀成功')
 
# 多数据库,用default的视图不受事务控制
@transaction.non_atomic_requests(using='default')
def seckill(request):
    return HttpResponse('秒杀成功')

2.2 视图开启事务

Django局部开启事务,借助于transaction.atomic

使用它我们就可以创建一个具备原子性的代码块,一旦代码块正常运行完毕,
所有的修改会被提交到数据库。反之,如果有异常,更改会被回滚
# fbv开启
from django.db import transaction
@transaction.atomic
def seckill(request):
    return HttpResponse('秒杀成功')
# cbv开启
from django.db import transaction
from rest_framework.views import APIView
class SeckillAPIView(APIView):
    @transaction.atomic
    def post(self, request):
        pass

2.3 局部使用事务

from django.db import transaction
def seckill(request):
    with transaction.atomic():
        pass
    return HttpResponse('秒杀成功')

2.4 savepoint回滚

'''
在事务操作中,我们还会经常显式地设置保存点(savepoint)
一旦发生异常或错误,我们使用savepoint_rollback方法让程序回滚到指定的保存点
如果没有问题,就使用savepoint_commit方法提交事务
'''

from .models import Book
from django.db import transaction
def seckill(request):
    with transaction.atomic():
        # 设置回滚点,一定要开启事务
        sid = transaction.savepoint()
        print(sid)
        try:
            book = Book.objects.get(pk=1)
            book.name = '红楼梦'
            book.save()
        except Exception as e:
            # 如发生异常,回滚到指定地方
            transaction.savepoint_rollback(sid)
            print('出异常了,回滚')
        # 如果没有异常,显式地提交一次事务
        transaction.savepoint_commit(sid)
    return HttpResponse('秒杀成功')

2.5 事务提交后回调函数

# 有的时候我们希望当前事务提交后立即执行额外的任务,比如客户下订单后立即邮件通知卖家

###### 案例一##################
def send_email():
    print('发送邮件给卖家了')
def seckill(request):
    with transaction.atomic():
        # 设置回滚点,一定要开启事务
        sid = transaction.savepoint()
        print(sid)
        try:
            book = Book.objects.get(pk=1)
            book.count = book.count-1
            book.save()
        except Exception as e:
            # 如发生异常,回滚到指定地方
            transaction.savepoint_rollback(sid)
        else:
            transaction.savepoint_commit(sid)
            transaction.on_commit(send_email)
    return HttpResponse('秒杀成功')
  
  
##### 案例二:celery中使用###
transaction.on_commit(lambda: send_sms.delay('电话号码'))

3 django中使用悲观锁

3.1 前置条件,表模型

from django.db import models
class Book(models.Model):
    name = models.CharField(max_length=32)
    count = models.IntegerField()

class Order(models.Model):
    order_id=models.CharField(max_length=64)
    order_name=models.CharField(max_length=32)
# Django中使用悲观锁锁定一个对象,需要使用select_for_update()方法。它本质是一个行级锁,能锁定所有匹配的行,如果查询所有,可以锁住整张表,直到事务结束

@transaction.atomic
def seckill(request):
    # 锁住查询到的book对象,直到事务结束
    book = Book.objects.select_for_update().filter(pk=1).first()
    book.count= book.count-1
    book.save()
    time.sleep(4)
    return HttpResponse('秒杀成功')

3.1 模拟秒杀生成订单场景

@transaction.atomic
def seckill(request):
    # 锁住查询到的book对象,直到事务结束
    sid = transaction.savepoint()
    book = Book.objects.select_for_update().filter(pk=1).first()  # 加悲观锁
    if book.count > 0:
        print('库存可以,下单')
        Order.objects.create(order_id=str(datetime.datetime.now()), order_name='测试订单')
        # 库存-1,扣减的时候,判断库存是不是上面查出来的库存,如果不是,就回滚
        time.sleep(random.randint(1, 4))  # 模拟延迟
        book.count=book.count-1
        book.save()
        transaction.savepoint_commit(sid)
        return HttpResponse('秒杀成功')
    else:
        transaction.savepoint_rollback(sid)
        return HttpResponse('库存不足,秒杀失败')

客户端测试

import requests
from threading import Thread
def task():
    res = requests.get('http://127.0.0.1:8000/sckill/')
    print(res.text)

if __name__ == '__main__':
    for i in range(10):
        t=Thread(target=task)
        t.start()

4 django使用乐观锁

4.1 普通版

@transaction.atomic
def seckill(request):
    # 锁住查询到的book对象,直到事务结束
    sid = transaction.savepoint()
    book = Book.objects.filter(pk=1).first()  # 没加锁
    count = book.count
    print('现在的库存为:%s' % count)
    if book.count > 0:
        print('库存可以,下单')
        Order.objects.create(order_id=str(datetime.datetime.now()), order_name='测试订单')
        # 库存-1,扣减的时候,判断库存是不是上面查出来的库存,如果不是,就回滚
        time.sleep(random.randint(1, 4))  # 模拟延迟
        res = Book.objects.filter(pk=1, count=count).update(count=count - 1)
        if res >= 1:  # 表示修改成功
            transaction.savepoint_commit(sid)
            return HttpResponse('秒杀成功')
        else:  # 修改不成功,回滚
            transaction.savepoint_rollback(sid)
            return HttpResponse('被别人改了,回滚,秒杀失败')

    else:
        transaction.savepoint_rollback(sid)
        return HttpResponse('库存不足,秒杀失败')

客户端测试

import requests
from threading import Thread
def task():
    res = requests.get('http://127.0.0.1:8000/sckill/')
    print(res.text)

if __name__ == '__main__':
    for i in range(10):
        t=Thread(target=task)
        t.start()

4.2 升级版

@transaction.atomic
def seckill(request):
    # 锁住查询到的book对象,直到事务结束
    ### 乐观锁可能会失败,我们一直尝试秒杀,直到秒成功或库存不足
    while True:
        sid = transaction.savepoint()
        book = Book.objects.filter(pk=1).first()  # 没加锁
        count = book.count
        print('现在的库存为:%s' % count)
        if book.count > 0:
            print('库存可以,下单')
            Order.objects.create(order_id=str(datetime.datetime.now()), order_name='测试订单')
            # 库存-1,扣减的时候,判断库存是不是上面查出来的库存,如果不是,就回滚
            # time.sleep(random.randint(1, 4))  # 模拟延迟
            res = Book.objects.filter(pk=1, count=count).update(count=count - 1)
            if res >= 1:  # 表示修改成功
                transaction.savepoint_commit(sid)
                return HttpResponse('秒杀成功')
            else:  # 修改不成功,回滚
                transaction.savepoint_rollback(sid)
                print('被别人扣减了,继续秒杀')
                continue

        else:
            transaction.savepoint_rollback(sid)
            return HttpResponse('库存不足,秒杀失败')

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>