Django与数据库
我叫补三补四,很高兴见到大家,欢迎一起学习交流和进步
今天来讲一讲alpha策略制定后的测试问题
mysql配置
Django模型体现了面向对象的编程技术,是一种面向对象的编程语言和不兼容类型能相互转化的编程技术,这种技术也叫ORM(Object Relation Mapping,ORM)对象关系映射,Django的ORM功能十分强大,极大提高了开发效率
在Django当中配置数据库的连接非常简单
settings.py文件当中会提前为你创建好一个DATABASE(类型为字典):
# 这是settings.py的内容
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 配置引擎
'OPTIONS': {
'read_default_file': '/path/to/my.cnf', # 配置文件路径
},
'USER': 'yonghu', # 数据库用户名
'PASSWORD': 'mima', # 数据库密码
'HOST': '127.0.0.1', # 数据库服务监听IP
'PORT': '3306', # 数据库服务监听端口
'NAME': 'data', # 数据库名字
}
}
配置连接有一定顺序
如果配置中定义了OPTIONS,其中定义的连接信息则会优先被使用,如果没有定义OPTIONS,则配置当中的USER、NAME、PASS WORD、HOST、PORT等信息会依次被使用到
DATABASE定义的数据库数量不受限制,但是必须定义一个名为default的数据库,数据库支持如下配置:
ENGINE:配置定义数据库后端:Django自带支持的后端有:PostgreSQL、MYSQL、SQLite、Oracle
HOST:指定连接数据库的主机地址,空字符串则输出默认的地址localhost,也可以使用指定用于连接的套字路径
NAME:数据库名
CONN_MAX_AGE:一个连接的周期,比如设置为5,则会在请求结束5秒后段开连接
OPTIONS:默认为空,用于连接字典的额外参数
PASSWORD:数据库密码,默认为空(用环境变量)
PORT:数据库的端口号
USER:连接数据库的用户名
连接池
在客户端向服务器发出访问请求以后,需要建立一个到数据端的连接,每进行一次访问操作,就会创建一个连接,在mysql当中,这样的连接由max_connection来配置。大部分情况下,这种连接是网络连接,链接过程当中会使用网络套接字,这是一个耗时的操作,大大增加了服务器的压力,为了缓解这种问题,我们可以使用连接池,也就是将数据库连接放到应用程序的缓存当中,在应用程序需要多数据库发出请求时,先从连接池获取连接,再使用这个连接请求数据库
简单来讲,连接池就是通过预先创建一些保持长期为打开状态的接口,从而减少频繁开关连接的开销,Django在首次进行数据库连接后,这个连接会长期在打开状态,只有当超过生命时间周期后才会被关闭,Django本身也并不具备连接池功能,但我们可以通过第三方库来实现,例如django-db-connection-pool 、psycopg2-binary、django_db_polling
实操
不同的库对于连接池的配置有不同的要求,这里用django-db-connection-pool
首先先下载:
pip install django-db-connection-pool
然后在settings.py当中添加相应的配置:
# settings.py
INSTALLED_APPS = [
...
'django_db_connection_pool',
...
]
DATABASES = {
'default': {
'ENGINE': 'django_db_connection_pool.backends.mysql', # 使用连接池的数据库引擎
'NAME': 'your_database_name',
'USER': 'your_database_user',
'PASSWORD': 'your_database_password',
'HOST': 'localhost',
'PORT': '3306',
'POOL_OPTIONS': {
'POOL_SIZE': 10, # 连接池大小
'MAX_OVERFLOW': 20, # 最大溢出连接数
},
}
}
Django迁移工具
Django提供的迁移工具可以将对模型所做的更改应用到数据库,从而修改对应的表单和数据库,Django当中提供了三个常用命令,分别是migrate、makemigration、sqlmigrate
其中,migrate命令负责将迁移应用到数据库;makemigration负责将模型变动转换成迁移;sqlmigrate会输出应用变动时实际执行的SQL语句。
具体使用:
1.`makemigrations`命令
`makemigrations`用于生成迁移文件,将模型的变更转换为迁移脚本。
基本用法
python manage.py makemigrations
此命令会检查所有已安装应用的模型定义,与数据库当前状态进行比较,并生成必要的迁移文件。
指定应用
如果只想为特定应用生成迁移文件,可以指定应用名称:
python manage.py makemigrations your_app_name
这会为`myapp`应用生成迁移文件。
查看迁移计划
在生成迁移文件之前,可以使用`--plan`选项查看即将执行的迁移操作:
python manage.py makemigrations --plan
这有助于了解迁移的具体内容。
2.`migrate`命令
`migrate`用于将迁移文件应用到数据库,更新数据库结构以匹配模型。
基本用法
python manage.py migrate
此命令会应用所有未应用的迁移文件。
指定应用
如果只想应用特定应用的迁移,可以指定应用名称:
python manage.py migrate your_app_name
这会只应用`myapp`应用的迁移。
3.`sqlmigrate`命令
`sqlmigrate`用于查看特定迁移文件对应的 SQL 语句,但不会实际执行这些语句。
基本用法
python manage.py sqlmigrate app_name migration_name
例如,查看`myapp`应用中名为`0001_initial`的迁移文件的 SQL 语句:
python manage.py sqlmigrate myapp 0001
这会输出该迁移文件对应的 SQL 语句。
进阶用法
• 指定数据库:
python manage.py sqlmigrate myapp 0001 --database=secondary_db
这会在多数据库环境中指定数据库。
• 列出迁移文件:
python manage.py sqlmigrate myapp --list
这会列出所有可用的迁移文件及其状态。
通过以上命令,你可以有效地管理 Django 项目的数据库迁移,确保模型与数据库结构始终保持一致。
数据库创建完成,Django会自动为模型提供一个数据库抽象API,允许创建,检索,更新和删除对象,因此,我们不需要学习mysql语言,仅仅靠Django当中提供的函数就能实现对数据库的增删改查操作,不管是增删还是改查,首先要对数据库进行保存,所以我们要学习Django当中的save()方法
简单案例:
python manage.py shell
from myapp.models import Person
person = Person(name="Alice", age=30)
person.save()
在该案例中在数据库当中新写入了一个person对象,同时,会自动获取一个自增的主键
获取数据表当中的对象:
all_products =Product.objects.all()
通过过滤器获取符合条件的子集:
Product.objects.filter(date_created__year=2018)
多个过滤器连接:
Product.objects.filter(date_created__year=2018).exclude(date_created__month=12)
这在sql里面是AND的语句表达,如果想用OR语句查询可以使用Q对象:
Product.objects.filter(
Q(title__startswith="Women") | (Q(date_created=date(2005, 5, 2)) | Q(date_created=date(2005, 5, 6)))
)
这段代码的目的是从一个名为 Product 的模型中检索一个对象,该对象满足以下条件之一:• 标题以"Women"开头。• 创建日期为2005年5月2日或2005年5月6日。
懒加载和缓存
QuerySet采用懒加载的机制(只有要用到结果时才会访问数据库),因此,访问数据库时,要采用遍历、切片、len()、list()、bool()等方式进行访问,为了降低数据库的负荷,每个QuerySet都会保留一份缓存
>>> print([p.date_created for p in Product.objects.all()])
>>> print([p.title for p in Product.objects.all()])
因此,两次输入 print([p.date_created for p in Product.objects.all()]) 的结果很可能不同
也可以只是对数据集做一些简单的运算并返回结果(可以用count()查询数量,用Avg查询平均值等等)
数据库事务
将数据库上的许多操作,例如读取数据库、对象、写入、获取锁封装成一个工作单元,整个单元内的操作要么都成功,要么都失败,这就叫事务,在Django当中,提供了控制数据库事务管理的方法,要启用其中将每一个请求包装在事务中的功能,可以在数据库的配置中设置:ATOMIC_REQUESTS为TRUE来开启此功能
此功能的大概工作流程如下:调用视图函数之前,Django会启动一个事务,如果生成的响应没有问题,Django就提交事务,否则就会回滚事务
(也可以使用atomic()上下文管理器):
from django.db import transaction
def my_view(request):
try:
with transaction.atomic():
# 在这里执行数据库操作
...
except Exception as e:
# 处理异常,事务会自动回滚
...
return HttpResponse('Success or Error')
from django.db import transaction
@transaction.atomic # 作为装饰器使用
def viewfunc(request):
do_stuff()
def anotherviewfunc(request):
with transaction.atomic(): # 作为上下文管理器使用
do_more_stuff()
需要注意:视图函数的执行包含在事务当中,但中间件和模板的渲染是在事务之外运行的
自动提交
自动提交(Autocommit)模式是指数据库在执行每个单独的 SQL 语句后立即将其提交到数据库。这意味着每个操作都是一个独立的事务,没有事务的开始和结束控制。这种模式通常用于简单的数据库操作,其中不需要复杂的事务控制。在 Django 中,默认情况下,数据库操作是自动提交的。这意味着如果你不显式地使用事务管理,每个数据库操作(如 .save() , .delete() , .update() 等)都会立即被提交。
可以在配置文件当中设置禁止
AUTOCOMMIT = False
提交后执行操作
在数据库操作中,"提交后执行操作"通常指的是在事务提交之后执行某些特定的操作。在 Django 中,这可以通过几种方式实现:
1.使用信号(Signals)
Django 的信号机制允许你在执行特定操作后发送信号,其他接收者可以监听这些信号并执行相应的操作。例如,你可以在事务提交后发送一个信号。
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=MyModel)
def my_model_saved(sender, instance, **kwargs):
# 这个函数会在 MyModel 实例保存后执行
# 确保在事务提交后执行
if kwargs.get('created', False):
# 实例创建后执行的操作
pass
# 在视图中使用事务
def my_view(request):
try:
with transaction.atomic():
my_model_instance = MyModel.objects.create(...)
# 其他数据库操作...
except Exception as e:
# 处理异常
pass
2.使用`transaction.on_commit`回调
Django 3.6+引入了`transaction.on_commit`回调,允许你在事务提交时执行某些操作。
from django.db import transaction
def my_view(request):
def callback():
# 事务提交后执行的操作
pass
try:
with transaction.atomic():
my_model_instance = MyModel.objects.create(...)
# 其他数据库操作...
transaction.on_commit(callback)
except Exception as e:
# 处理异常
pass
在这个例子中,`callback`函数将在事务成功提交后执行。
3.使用数据库触发器(Triggers)
对于更复杂的场景,你可以在数据库层面使用触发器(Triggers)。触发器是数据库对象,它们在特定事件发生时自动执行。例如,你可以创建一个触发器,在插入或更新记录后执行某些操作。
CREATE TRIGGER after_insert_my_model
AFTER INSERT ON my_model
FOR EACH ROW
BEGIN
-- 执行操作
END;
请注意,使用触发器需要对数据库有更深入的了解,并且不同数据库的触发器语法可能不同。
注意事项
• 确保在事务提交后执行的操作不会违反事务的隔离性或一致性。
• 使用信号或回调时,确保它们不会引入死锁或循环依赖。
• 在使用触发器时,确保它们不会与应用逻辑冲突。
通过这些方法,你可以在 Django 中实现在事务提交后执行特定操作的需求。选择哪种方法取决于你的具体场景和偏好。
数据库并发控制
多用户同时访问和更改数据库可能会引发冲突,因此,要协调同步事务,在两个活动尝试修改同一个数据库的对象时,有三种情况,两个活动会相互干扰:
- 脏读:事务A读取到了事务B的修改后尚未提交的数据,但事务B如果最后没有选择提交,那这种读取方式就叫脏读
- 不可重复读:多次读取同一个数据,两次读取结果不一致,这种情况一般发生在其他事务对该数据进行修改并提交
- 幻读:A查询后的下一刻数据库被更改,导致查询结果不准确
越是允许缓存当中的过时数据存在,则并发的线程/问题就越多,可能出现的问题也就越大
这会导致什么问题?具体举个例子,A访问数据库时余额为10,然后B也访问了数据库,A从中取出3,也就是原数据库改为7,但B如果进行存5的操作,此时由于数据库并发的问题,B修改数据库为10+5=15。
为了杜绝此类问题出现,需要确保正在处理的资源在工作时不会发生改变
悲观锁
是指实体在应用中存储的整个周期,数据库都处于锁定状态,以此来限制其他用户使用这个数据库当中的实体
可以是修改数据库的人不希望其他人再次过程当中读取实体,也可以是读取数据库的人不希望其他人在此过程当中修改实体
锁的范围可能是整个数据库、表、多行或单行。这些锁分别称为数据库锁、表锁、页锁和行锁。
优点是简单易用,缺点是当用户数量过多时该方法限制了可同时操作的用户数量
在Django中实现悲观锁可以通过`select_for_update()`方法来完成,它利用数据库的行级锁机制,确保在事务提交或回滚之前,其他事务无法修改被锁定的行。
实现步骤
1. 使用`transaction.atomic()`装饰器:确保操作在一个事务中完成,如果操作失败,事务会自动回滚。
2. 调用`select_for_update()`方法:对查询结果加锁,锁定特定的行。
3. 执行业务逻辑:在锁定期间对数据进行修改。
4. 提交或回滚事务:如果操作成功,事务提交;如果失败,事务回滚。
示例代码
以下是一个使用悲观锁的Django视图函数示例,模拟秒杀场景:
from django.db import transaction
from django.http import HttpResponse
from .models import Book, Order
import time
import random
@transaction.atomic
def seckill(request):
sid = transaction.savepoint() # 设置保存点
book = Book.objects.select_for_update().filter(pk=1).first() # 加行锁
if book.count > 0:
print('库存可以,下单')
Order.objects.create(order_id=str(time.time()), order_name='测试订单')
time.sleep(random.randint(1, 4)) # 模拟延迟
book.count -= 1
book.save()
transaction.savepoint_commit(sid) # 提交事务
return HttpResponse('秒杀成功')
else:
transaction.savepoint_rollback(sid) # 回滚事务
return HttpResponse('库存不足,秒杀失败')
注意事项
1. 锁的粒度:`select_for_update()`默认加行级锁,但如果查询条件未指定主键或唯一索引,可能会升级为表锁。
2. 死锁风险:在高并发场景下,悲观锁可能导致死锁。需要合理设计事务逻辑,避免长时间持有锁。
3. 数据库支持:`select_for_update()`需要数据库支持事务和行级锁,如MySQL的InnoDB引擎。
通过这种方式,Django可以利用数据库的锁机制实现悲观锁,确保在并发场景下数据的一致性和完整性。
乐观锁
在冲突发生不频繁时,可以考不阻止并发控制,而是选择检测冲突,并且在冲突发生时去解决他
updated = Account.objects.filter(
id=self.id,
version=self.version,
).update(
balance=F('balance') + amount,
version=F('version') + 1,
)
在原本定义的模型上多加上一个version,update语句其实暗含了检查版本的功能(因为会先检查version字段与传入的version字段是否匹配,只有匹配才会执行更改)
解决冲突的基本途径有:放弃、展示问题让客户决定、合并改动、记录冲突让后来的人决定、无视冲突直接覆盖,在不同的情况下,我们可以选择不同的锁和不同的策略
数据库扩展
纵向扩展
简单来说,数据库的扩展就是让数据库能够处理更多流量和更多读写查询,主流的扩展方法有纵向扩展和横向扩展,纵向扩展采用的是增强单个数据库能力的方法(增强cpu、内存、存储空间等等)
优点:应用层不需要适配;缺点:硬件扩容有上限,成本高
横向扩展
横向扩展采用的是将多个数据库组合起来的方法,和纵向扩展相比,此发可用性高,易于升级且成本低廉,缺点是对技术要求高并且要求对应用层做出更改
几种横向扩展的方法
读写分离
读写分离主要用到了MYSQL的复制功能
允许将一个来自MYSQL数据库服务器的数据自动复制到一个或多个MYSQL数据库服务器,该功能需要在修改Django配置的同时对于MYSQL也做出小的修改,效果是当一台数据库服务宕机后,能通过调整另外一台库以最快的速度恢复服务,由于主从复制是单向的,因此只有主数据库用于写操作,而读操作则可以在多个数据库上进行,因此,我们至少需要两个数据库,一个用于读操作,另一个用于写操作,要使复制功能正常工作,首先要把复制事件写入日志,一般称这个日志为Binlog,每当从数据库连接到主数据库,主数据库就会创建新的线程,然后执行服务器对他的请求,大多请求会是将Binlog传给从服务器并且通知从服务器有新的Binlog写入
从服务器会起两个线程来处理复制,一个称I/O线程,负责连接到主服务器从中读取二进制日志事件(中继日志),另一个称SQL线程,负责在本地的中继日志当中读取事件,然后本地执行
在Django当中提供了多数据库请求路由来实现读写分离
使用步骤如下:
- 修改settings.py当中的DATABASES列表
DATABASES = {
'default': { # 默认数据库,主数据库
'NAME': 'multidb',
'ENGINE': 'django.db.backends.mysql',
'USER': 'some_user', # 数据库账号
'PASSWORD': 'some_password', # 数据库密码
'HOST': 'master_host_ip', # 数据库IP
},
'slave': { # 从数据库
'NAME': 'multidb',
'ENGINE': 'django.db.backends.mysql',
'USER': 'some_user', # 数据库账号
'PASSWORD': 'some_password', # 数据库密码
'HOST': 'slave_host_ip' # 数据库IP
}
}
- 将写操作应用到主数据库,读操作应用到从数据库
import random
class RandomRouter:
def db_for_read(self, model, **hints):
return random.choice(['replica1', 'replica2']) # 随机选一个从数据库
- 在settings.py当中加上
DATABASE_ROUTERS = ['path.to.DefaultRouter']
垂直分库
比较常见的一种做法是将不同的模块放在不同的数据库中。
通过路由,将不同模块的请求转到不同的数据库当中,首先在settings.py当中配置多个数据库:
DATABASES = {
'default': {},
'auth_db': {
'NAME': 'auth_db',
'ENGINE': 'django.db.backends.mysql',
# 其他配置...
},
'product_db': {
'NAME': 'product_db',
'ENGINE': 'django.db.backends.mysql',
# 其他配置...
},
'order_db': {
'NAME': 'order_db',
'ENGINE': 'django.db.backends.mysql',
# 其他配置...
},
}
然后根据模型的不同路由请求到不同的数据库
class MultiDatabaseRouter:
def db_for_read(self, model, **hints):
if model == User: # 请求用户数据库
return 'auth_db'
elif model == Product: # 请求商品数据库
return 'product_db'
elif model == Order: # 请求订单数据库
return 'order_db'
else:
return 'default'
在settings.py当中加上
DATABASE_ROUTERS = ['path.to.MultiDatabaseRouter']
水平扩展
当用户数量达到一定规模,单个数据库或数据表无法容纳用户的全部数据,常见的做法就是对数据库中的数据进行水平分区,每个单独的分区称为分片
每个分片承载单独的数据库,以分散负载
分片操作虽然提高了查询性能,单本身也使应用层变得极其复杂,采用该策略需要慎重考虑
常见的分片方式有算法分片和动态分片——算法分片是让数据通过算法写入不同分片,动态分片则需要客户端读取其他存储,确认分片信息,最简单的算法分片是取模算法,因为笔者做不到每天300万的用户访问量所以这部分咱就不讨论了...