再试Django-进阶

之前接触了一次Django 👉 初识Django,这里我会再次使用Django编写一个功能更加完善的后台。

前言

之前使用Django编写了一个脸部识别的后端Demo, 现在要完善这个Demo, 让它变得更加的健全和多功能, 上篇文章讲过的部分在此不再赘述.

远程开发进阶

‘初识Django’文中我们使用了PyCharm进行了远程开发, 具体配置操作请见使用Jetbrains系列IDE进行远程开发一文.

其实, 我们也可以在远程开发的时候使用远程的python虚拟环境, 还是基于virtualenv, 我们只要在配置remote interpreter的时候选择venv/bin/python*作为翻译器就可以了.

使用virtualenv生成虚拟环境的时候需要注意是要python2还是3的环境, 需要使用-p参数明确指出相应版本python可执行文件的绝对路径, 也可以使用--no-site-packages参数来使虚拟环境不能访问之前系统中全局安装的库, 或是使用--system-site-packages参数来允许虚拟环境可以访问之前系统中全局安装的库.
注: 全局安装的库无法在虚拟环境中进行删除

序列化与反序列化策略

读: 读redis, 有数据直接返回, 没有数据则读mysql, 有数据则返回数据并把数据写到redis.

写: 直接写mysql, 写入成功再写redis。

MySQL

mysql数据库的使用

首先我们要安装django需要的mysql相关的库, pip3 install pymysql

接着我们在项目配置文件__init__.py中加上配置代码 👇

import pymysql

pymysql.install_as_MySQLdb()

我们需要在使用的数据库服务器端创建一个本项目专用的库, 这里我就直接以项目名作为数据库名了, 以及项目数据库用户, 建议是创建一个专门的用户, 然后仅将项目的库全部授权给他.

然后我们配置连接数据库时候使用的库, 用户名和密码, 项目配置文件settings.py中的

# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',   # 注: Django默认使用的是自身集成的sqlite数据库
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

修改为 👇

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': '数据库名',
        'USER': 'mysql用户名',
        'PASSWORD': '用户名密码',
        'HOST': '数据库服务器IP地址',
        'PORT': '3306',
    },
}

这都是一般开发的套路了, 不再多说什么.

最后运行python manage.py makemigrationspython manage.py migrate命令创建表.

这里需要注意的是, 在构造表时如果你有类似CharField这种字段, 那么要给它设置一个默认的值(如unique_id = models.CharField(max_length=100, default='UNIQUE_ID')), 否则在运行python manage.py makemigrations时会遇到报错You are trying to add a non-nullable field '***' to facecharacteristic without a default; we can't do that (the database needs something to populate existing rows).

Redis

Redis作为缓存

安装django-redis 👉 pip3 install django-redis

配置文件中写入使用缓存 👇

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://redis服务器IP地址:6379',
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            # "PASSWORD": "123456",  # 配置密码, 没有的话不必写
        },
        # "KEY_PREFIX": "face",   # 键名的前缀, 没必要的话可以不设置
    },
}

Redis基础操作

键名设计, 推荐业务名:对象名:id:[属性], 分别对应数据库的库名:表名:id:属性, 如果redis只被一个业务使用, 则可以省略业务名, 如果键名较长, 可以适当缩略从而减少内存浪费

设置键值对 set key value [ex seconds] [px milliseconds] [nx|xx] ex: 设置秒级过期时间, px: 设置毫秒级过期时间, nx: 键必须不存在才能设置成功,用于添加, xx: 键必须存在才能设置成功,用于更新; redis 还提供setexsetnx两个命令, 作用同ex 和xx, setex key seconds value, setnx key value

获取值, get key, 键不存在返回nil

批量设置键值, mset key value [key value ...]

批量获取键值, mget key [key ...]

查看所有键 keys *

检查键是否存在 exists key, 存在则返回1, 否则返回0

键总数 dbsize, 返回当前数据库中键的总数

删除键 del key [key...] 通用命令, 无论什么数据结构类型, 都能删除, 返回结果为成功删除的键的总数

键过期 expire key seconds 添加键的过期时间, 过期后自动删除,
ttl key命令查看键的剩余时间, 返回-1则键未设置过期时间, -2则键不存在

键的数据类型 type key, 键不存在返回none

查询键的数据结构的内部编码object encoding key

flushall清空所有缓存

incr key 自增操作, 值非整数则返回错误, 值是整数则返回自增后的值, 键不存在则按值为0自增, 返回结果为1
类似的decr key自减操作, incrby key increment自增指定数字, decrby key decrement自减指定数字, incrbyfloat key increment自增浮点数

append key value向字符串键尾部追加值

strlen key 字符串长度

等等…

以后会专门写一篇关于reids的入门文章, 自己关于这方面的知识还有待补充, 先立一个flag.

关于键的前缀

在Django中你使用cache.set(key, value)函数将键-值保存到redis缓存中的时候, django内部还会对键的名称进行一系列的转化, 默认的就是KEY_PREFIX为空, 然后在键名前面加上:1:, 所以如果你用cache.set("my_key", "1")将键值保存到redis后, 再使用redis-cli到redis中使用keys *去查询的话, 你会发现多了个名为:1:my_key的键, 其值为1

说这点的原因是, 你使用cache.get(key)去查询的时候是不用加上:1:的, django会自动帮你转换, 所以你知道了, django只能查询到redis缓存中键名符合它规则的键值, 默认也就是以:1:开头的键, 所以如果你直接在redis中添加数据的话, 不要忘了要符合这个规则, 否则使用django.core.cache模块是无法查询到的(当然了, 直接使用redis模块肯定是可以的, 因为它跟django没关系嘛)

但是以上只限于键的值为int类型, 如果你直接在redis中添加的键的值是string类型的话, 你在django使用cache.get()的时候会得到_pickle.UnpicklingError: could not find MARK的错误, 而你如果用cache.set()保存string值, 再reids-cli中查询该键的值, 会发现它除了你设定的值之外还有一串的编码, 如我用cache.set("jay's_key", "jay")设定后再使用redis-cli查询该键会得到"\x80\x04\x95\a\x00\x00\x00\x00\x00\x00\x00\x8c\x03jay\x94.", 这是因为django在序列化的时候使用了Pickler库, 这里我们可以选择使用其他的序列化器来将数据进行持久化而避免多余的mark编码的出现, JSONSerializer就是一个很好的选择, 因为现在json的数据结构用的越来越多, 本项目中的数据存储也使用的是json.

我们对settings.py配置文件中的CACHES选项修改成如下

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://172.17.0.3:6379',
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "SERIALIZER": "django_redis.serializers.json.JSONSerializer",  # 修改序列化器为json serializer, 弃用默认的pickle
        },
    },
}

mysql更新数据到redis

这里我使用mysql2redis实现mysql数据库自动更新数据到redis缓存

详情请见另一篇我的博文使用Mysql2Redis自动更新数据到Redis

设置mysql触发器

查看已有的触发器

选中使用数据库后使用命令SHOW TRIGGERS;, 或者直接使用命令SHOW TRIGGERS FROM database_name;

删除触发器

DROP TRIGGER [IF EXISTS] [database_name.]trigger_name, 最好加上if exists, 否则如果触发器不存在的话会报错.

JWT

jwt的配置使用直接参考 👉 简书该文

关于JWT的有效期, 默认设置(无需配置)是

'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),

JWT_EXPIRATION_DELTA是指发出的token的有效期为5分钟(300秒), 而JWT_REFRESH_EXPIRATION_DELTA是获得token后在7天内可以通过刷新token以延长它的有效期, 简单来说就是, 在获得一个token后, 如果不刷新它, 它的有效期是5分钟, 如果在5分钟内刷新它, 那么它的有效期将从刷新完毕后开始往后延长5分钟, 但是就算你一直刷新, 这个token的有效期从发出开始计算也一共只有7天

部署

由于django自带的开发服务器性能并不可靠, 所以在生产环境中进行部署时, 我们需要使用其他的高性能服务器.

uWSGI服务器

使用命令pip3 install uwsgi安装uwsgi,

其实, 我们在新建django项目的时候, 框架已经帮我们在项目配置目录下生成了wsgi.py配置文件(如本项目的Face_Eye/wsgi.py), 我们再需要编写配置文件uwsgi.ini配置文件帮助uWSGI服务器启动即可: 在项目配置目录下新建uwsgi.ini(文件名无所谓)ini配置文件, 其中加入如下配置内容(以下为本项目配置文件, 以供参考, 请根据自己的实际情况更改)

[uwsgi]
# 协议, 端口
http = :8000
# 项目所在目录(绝对路径)
chdir = /home/jay/face_eye
# 项目wsgi配置文件
module = Face_Eye.wsgi
# 启动主线程
master = true
# 最大工作进程数量
processes = 4
# 当服务器退出时自动删除unix socket文件和pid文件
vacuum = true
# 被卡住的进程经过如下时间(秒)后销毁
harakiri = 30
# 指定工作进程名称的前缀, 便于识别
procname-prefix-spaced = face_eye

# 不要使用root权限运行uWSGi
# uWSGI服务器运行时的用户
uid = jay
# uWSGI服务器运行时的用户组id
gid = jay

然后在项目配置目录下使用命令uwsgi uwsgi.ini启动uWSGI服务器, 即可访问相关端口.

关于为什么不要使用root权限运行uWSGI, 网上有如下解释”uWSGI试图在一切可能的情况下(滥)用 fork() 调用的写时拷贝语义。默认情况下,它会在加载你的应用之后进行fork,以尽可能的共享它们的内存。”,那就是内存安全方面的问题了, 听话就是了.

关于uWSGI的操作可以参考 👉 传送门

关于WSGI, uwsgi和uWSGI可以阅读文章: 做python Web开发你要理解:WSGI & uwsgi, 简而言之, uwsgi和WSGI都是通信协议, 而uWSGI是实现了uwsgi、WSGI和http等协议的Web服务器, 在此不再多加赘述.

nginx代理

有些时候只用uWSGI服务器就足够了, 但是还有些情况下, uWSGI的功能并不能满足我们的需求, 如对大量静态资源请求的处理, 安全方面的问题等, 在uWSGI前面加上nginx服务器进行代理会是很好的选择.

赘述

存在的问题

在测试中, 系统在使用redis缓存后的读取速度反而比从mysql中直接读取的速度慢, 原因可能从redis取数据的操作存在循环的问题, 问题的根源在于django-redis库没有提供一次性从reids中取出多条数据的方法, 使用其他的库应该可以消除该问题.,