django项目如何连接前端_如何保证 Django 项目的数据一致性

16bdcc47020d48877164308ed1aff342.png

0x00 前言

本文是《提升你的 Python 项目代码健壮性和性能》系列的第三篇文章。

  • 第一篇,讲的是如何 用 Type Annotation 提升你的 Python 代码健壮性
  • 第二篇,讲的是如何通过测试提升 Python 代码的健壮性

第三篇,即本文,我们来谈谈 Django 项目并发可能带来的问题以及如何保持 Django项目的数据一致性。

本文目录如下:

   0x00 前言 : section
▼ 0x01 背景知识 : section
		并发会带来数据不一致 : section
▼ 0x02 Django 项目如何解决项目 : section
		悲观的方式 : section
		乐观的方式 : section
		没有银弹 : section
	0x03 解决超卖问题 : section
▼ 0x04 番外篇 数据库隔离级别 : section
		READ-UNCOMMITTED : section
		READ-COMMITTED : section
		REPEATABLE-READ : section
		SERIALIZABLE : section
	0xEE 参考链接 : section

本文讨论的场景如下:

一个简单的秒杀系统,商品还剩 200 件。有一些用户来访问并下单。

这个项目的接口的简单写法就是:

@db_transaction
def user_order_by_product(user,product):
    if product.quantity > 0:
        product.quantity = product.quantity - 1
        make_order(user,product)
        product.save()

显然,这个写法确实简洁。

但问题就来了。

商品会超卖, 除非你的应用没什么人访问。

为什么呢?

0x01 背景知识

并发会带来数据不一致

看图

bff4f9804022f38ca1921f8ea55ee444.png

知道问题出在哪儿了吧?

  • 理解了?
  • 你确定理解了?
  • 你确定你真的理解了?

bd620cd54df566ae0911bdc15694e35f.png

其实是我给读者挖了一个坑,我画的这张图其实是有预设的,比如 NOTE2 处确保了 B 用户读到的是 2。

如果数据库隔离级别是 read uncommitted, NOTE2 处读到的也有可能是 1 ,

本文仅仅针对于 read committed 隔离级别下的 MYSQL / PostgreSQL

在上图中。

一般人写程序

  1. 往往不是用 Django 的 F 表达式,来实现 update balance = balance - 20 的操作。 update balance = balance - 20 where id = 1
  2. 而是计算 出新的 balance 然后 user.balance = 80, 接着 user.save()
这就会放大了问题

在低并发量的情况下,这个用户手动不断的下单,下单到 200 的时候,后端准时的报卖完了。

假如我现在是 20 个用户同时在下单,很可能机会出现上图的情况。

出了问题要解决问题 看到问题就要想法子
  • 鲁迅说过,一见短袖子,立刻想到白臂膊,立刻想到全 X 体,立刻想到....
  • 我也说过,提到并发,就想到锁,就想到乐观锁,就想到悲观锁。

0x02 Django 项目如何解决项目

有乐观的方式和悲观的方式,即所谓的乐观锁和悲观锁. 是不是有高下之分呢? 不一定.

这就好比乐观和悲观本身也并不见的有高下之分. 如同有的人乐观, 并不见得一定就是乐观, 搞不好是傻乐呵, 有的人的悲观, 只是底色比较悲凉, 但内心还是积极向上的.

本小节内容依照自己的理解厚颜无耻的援引了这篇文章的代码

https://medium.com/@hakibenita/how-to-manage-concurrency-in-django-models-b240fed4ee2

悲观的方式

悲观的方式就是锁住某个资源,不让其他人使用(排他),直到完成工作后释放。

为什么使用数据库的锁(准确来说是关系型数据库的锁)

  1. 数据库非常擅长处理锁来完成数据一致性。
  2. 数据库级别的锁可以保护其他进程修改数据。
@classmethod
def deposit(cls, id, amount):
   with transaction.atomic():
       account = (
           cls.objects
           .select_for_update()
           .get(id=id)
       )

       account.balance += amount
       account.save()
    return account

@classmethod
def withdraw(cls, id, amount):
   with transaction.atomic():
       account = (
           cls.objects
           .select_for_update()
           .get(id=id)
       )

       if account.balance < amount:
           raise errors.InsufficentFunds()
       account.balance -= amount
       account.save()

   return account

使用 select_for_update 锁住这个 object 直到事务结束

在使用悲观锁的情况下,存钱和取钱流程如下

6edfcf7f474c592ae4008d6dd806646c.png

乐观的方式

乐观的方式就是新建一个 version column, 每次修改余额的时候,版本增 1

同样我厚颜无耻的援引了 hakibenita 的代码

def deposit(self, id, amount):
   updated = Account.objects.filter(
       id=self.id,
       version=self.version,
   ).update(
       balance=balance + amount,
       version=self.version + 1,
   )
   return updated > 0

def withdraw(self, id, amount):
   if self.balance < amount:
       raise errors.InsufficentFunds()

   updated = Account.objects.filter(
       id=self.id,
       version=self.version,
   ).update(
       balance=balance - amount,
       version=self.version + 1,
   )

   return updated > 0

django 默认会返回修改成功的行数,于是,是不是存取成功,就看 updated 是否大于 0 了

没有银弹

计算机世界里面,多快好省的场景就不存在。一切看场景。

同样在并发量大的情况下

1. 如果对某几行修改比较频繁,版本更新频繁,可能乐观锁的 retry 就比较浪费了。
2. 如果是对整张表的更新比较频繁,而不是频繁修改某几行。乐观锁,就比较合适了。

  • 乐观方式在应用层,无法阻拦数据库的操作。不会存在死锁的问题。
  • 悲观方式是数据库实现,他阻止数据库写操作。

0x03 如何解决超卖问题

  1. 把数量和已卖放到 redis 里面呢?
  2. 交给 deamon 呢?
  3. 用 celery 然后排个队异步任务呢?

搞个再复杂一点点的,

  1. 在 Redis 里面直接生成 200 个订单号
  2. 然后用户来一个取走一个订单号码
  3. 通过 Celery 削峰 排队走异步任务
  4. 最后通过数据表的 uniq 约束来防止下单超过 200 个。

嗯,就是这么简单。

0x04 番外篇 数据库隔离级别

提到了数据库隔离级别, 就利用上面的例子顺手讲解一下数据库隔离级别吧.

READ-UNCOMMITTED

95ed9d0ae6c405bbe593f9bc42f23291.png

READ-COMMITTED

24c4af4ce9f0458865c72893e6284822.png

REPEATABLE-READ

eea42de2452254c110d506ac605b1211.png

SERIALIZABLE

这个就不放图了. 没啥好讲的. 性能太低.... 我是基本上没怎么使用过的

性能上 RU > RC > RR > S

一般人用 RC 和 RR 会多一些, 比如我的项目里就使用了 RC , 但什么时候我可能会考虑用 RR 呢? 比如, 我想在一个Session里面选两次 最近两个月的用户数据, 但是并不希望 出现新的用户.

0x05 番外篇 Django ORM

评论区 @灵魂对撞机 提出了防止超卖的另一种解决方案

先做订单记录

面向业务设计的表,和面向数据分析的表应该是两种设计思路。orm 是实体和记录的映射,比较适合面向业务设计的表。

@灵魂对撞机 也提到表关联查询太痛苦了

在我的认知里

  1. Django 本身是支持 RawSQL 查询的
  2. 当你想要的 object 和 row 是一一对应关系的时候,ORM 写起来特别舒服
- django orm 写过滤条件是很舒服的。(抛开性能来说)
- 针对 Object 的修改也很方便,比如商品数量减少。比如自增 update。

这也是 orm 的不足之处,显式多表连续 join 的话过滤条件就很麻烦。

如果需要关联查询这也应该看情况

  1. 如果一般的多表连续 join 如果能使用 nested queries 的话,用 Django 写起来也是特别的舒服。(如果你不是使用 mysql 这种对 nested query 几乎无优化的数据库)

```

VoteActivity.objects.filter(category=obj.category, user__name_contains("王"), user__city__type="一线城市") # 伪代码

```

2. 或许是应该走ETL或者是把数据丢到ES或者针对查询优化的表会更加合适?

当然, 这也就看具体的情况了.

0xEE 参考链接

  1. https://www.cnblogs.com/huanongying/p/7021555.html
  2. Django and MySQL
  3. https://medium.com/@hakibenita/how-to-manage-concurrency-in-django-models-b240fed4ee2
  4. Photo bySepp RutzonUnsplash
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值