git与redis
git
git介绍和安装
1.在企业中写一个接口,完成一个功能后,会把代码提交到远程仓库中
2.下载
https://git-scm.com/downloads
-下载完后在任意位置右键,会有两个东西【git gui here】、【git bash here】)
pycharm中配置git
1.如果使用的是pycharm,直接在打开配置pycharm
-在setting中搜索git,配置get安装可执行文件
2.下载开源软件:vcs--->get from version contral--->填入路径---》clone下来即可
svn,git ,github,gitee,gitlab
1.svn
版本管理软件,它是集中式的版本管理,必须有个svn的服务端,服务端如果过来,svn就用不了了
2.git
版本管理软件,它是一个分布式的版本管理,每个客户端都可以作为服务端,即便服务端挂了,也能进行版本管理(本地管理)
3.github
-全球最大的开源远程git仓库,全球最大的开源仓库,git远程仓库
-如果要写开源软件,本地装git,把代码提交到github
4.gitee
中国最大的开源软件仓库
5.gitlab
公司内部自己搭建的远程仓库,由运维负责搭建
git工作流程
# https://www.cnblogs.com/liuqingzheng/p/15328319.html
1.工作流程
-只要被git管理了,只要文件发送变化(新增、删除、修改),使用git就能看到它变成红色
-工作区的变更,要提交到暂存区,就会变绿
-从暂存区提交到版本库,才会被版本管理,只要被版本管理,之后就可以回退到某个版本
-可以把版本库的内容,提交到远程仓库,统一管理起来(所有开发者都可以提交)
-本地可以拉取远程的代码
-本地可以把本地代码提交到远程
'''
1.有红色信息(工作区有内容),就可以执行add
2.全绿信息(内容全部在暂存区),才执行commit
3.只有被版本库控制的代码,才能被监听,才可以回滚到任何一个版本
'''
git常用命令
1.初始仓库(执行这条命令,这个文件夹就变成了git仓库)
git init
2.在仓库中加入文件、修改文件
s1.py
3.查看状态的命令
git status
4.配置用户信息(如果局部没有则使用全局,如果全局也没有则报错)
4.1局部配置(只针对当前仓库)-->放到(当前仓库.git文件夹下的config)
git config user.name 'barry'
git config user.email '123@qq.com'
4.2全局配置(针对于所有仓库)-->放到(C:\用户\用户文件夹\.gitconfig新建用户信息,在所有仓库下都可以使用)
git config --global user.name 'barry'
git config --global user.email '123@qq.com'
5.把工作区内容提交到暂存区
-把工作区所有的更改都提交到暂存区
git add.
-把工作区当前文件的更改提交到暂存区
git add 文件名
6.把暂存区提交到版本库(一定要加注释,不加提交不了)
-把暂存区所有的数据都提交到版本库
git commit -m '注释'
7.查看状态
git status
git status -s # 简略显示
8.例子:修改s1.py在提交到版本库
8.1修改s1.py
8.2git status # 红的
8.3git add. # 提交到暂存区
8.4git commit -m '提交了s1.py' # 提交到版本库
8.5git status # 没有颜色
9.查看版本情况(查看本地版本库有哪些版本,一个版本有一个唯一的id号)
git log
git reflog # 更精简的显示
10.修改文件后,它变红了,在把他该回去
-把当前所有更改都回退,不包含新增的文件
git checkout .
11.例子:修改s1.py在提交到版本库
11.1git add . # 提交到暂存区
11.2git status # 绿的
11.3git reset HEAD # 将s1从暂存区拉回工作区
11.4git status # 红的
12.把版本库内容,回退到暂存区
git reset --soft 上一个版本号
13.从版本库拉回到工作区(能看懂红色的内容)
git reset --版本号
git reset --mix 版本号
'''
重点:
git init
git status(git status -s 简略显示)
git log
git reflog
git add .
git commot -m ''
git reset --hard 版本号
'''
14.补充
-空文件夹不会被git记录,如:文件夹为空是不会在git status中显示
-如果删掉当前仓库的用户,就会使用系统的用户,git log可以看到是哪个用户提交的
14.1查看时间点之前|之后的日志
>: git log --after 2018-6-1
>: git log --before 2018-6-1
>: git reflog --after 2018-6-1
>: git reflog --before 2018-6-1
14.2查看指定开发日志
>: git log --author author_name
>: git reflog --author author_name
14.3回滚到上一个版本
>: git reset --hard HEAD^
>: git reset --hard HEAD~
14.4回滚到上三个版本
>: git reset --hard HEAD^^^
>: git reset --hard HEAD~3
14.5回滚到指定版本号的版本
>: git reset --hard 版本号 (重点)
>: eg: git reset --hard 35cb292
git 过滤文件
1.在项目中,有些文件(.pyc)、文件夹(node_modules)(.idea)是我们不希望被版本管理了,所以我们要忽略它
2.在仓库中新建 .gitignore ,注意这个文件没有任何后缀名,在文件中配置
2.1在仓库的根目录下创建该文件
2.2文件与文件夹均可以被过滤
2.3文件的过滤语法
'''
1.过滤文件内容
文件或文件夹名:代表所有目录下的同名文件或文件夹都被过滤
/文件或文件夹名:代表仓库根目录下的文件或文件夹被过滤
2.eg:
a.txt:项目中所有a.txt文件和文件夹都会被过滤
/a.txt:项目中只有根目录下a.txt文件和文件夹会被过滤
/b/a.txt:项目中只有根目录下的b文件夹下的a.txt文件和文件夹会被过滤
*a*:名字中有一个a的都会被过滤(*代表0~n个任意字符)
空文件夹不会被提交,空包会被提交,包可以被提交(包中有一个init空文件)
'''
3.luffy后端项目要被git管理
3.1在项目路径下敲git init
3.2创建 .gitignore
3.3git add . 提交
3.4git commit -m '初始化项目' 提交到版本库
git多分支开发
1.在公司中,有几个人有情开发一个项目,开发过程中项目还不能运行时,客户想看开发进度
-分支:通过分支,更方便协同开发,管理版本
2.分支操作
2.1查看分支
git branch # *和绿色表示你当前所在的分支
2.2创建分支
-git branch dev
2.3切换分支
- git checkout dev
2.4删除分支
-git branch -d dev # 在其他分支上才能删除
2.5合并分支(可能会有冲突)
-把dev合并到master上
-回到master上,开始执行代码
git merge dev
3.查看远程分支
git branch -a
git远程仓库
1.远程仓库
github # 很慢
gitee # 很快,比较常用
gitlab # 公司直接搭建的,搭建步骤:https://zhuanlan.zhihu.com/p/413217715
2.gitee
2.1新建一个远程仓库
-只需要写仓库名字,其他的都不需要填,仓库最好是空的(如果不是空的,操作起来会很麻烦)
2.2开源协议
-GPL、Apache
-GPL:自由使用开源的软件修改、增加内容,可以收费,但是你的代码必须开源
-Apache:可以使用开源的软件,但必须注明使用了
2.3本地代码,上传到远程仓库
-本地有仓库,已经有版本管理了,远程有仓库,是空的
-设置用户名和邮箱(如果不按照它显示的设置,在右侧贡献者看到别人的头像)
git config --global user.name "barry"
git config --global user.email "123@qq.com"
2.4已有仓库
-新增一个远程仓库
git remote add origin https://gitee.com/liuqingzheng/lqz.git
-把本地代码推上去
git push origin master # 把本地的主分支推到origin对应的远程分支
-第一次推是需要输入gitee的账号和密码,之后就不需要在输了
-gitee用户记录到凭证管理,可以删除,可以修改
把项目传到远程仓库(非空的)
1.创建远程仓库luff_api
1.1把远程仓库内容克隆到本地
git clone https://gitee.com/speechlessabc/luffy_api.git
1.2把我们项目的代码,copy到luffy_api文件夹下(.git文件夹不要copy)
1.3git add .
1.4git commit -m '初始化仓库'
1.5git push origin master
2.之后,每完成项目功能就要执行下面的命令
git add .
git commit -m '完成了xx功能,修改了xx文件' # 提交到本地仓库
2.1每次提交(推到远端)代码之前,要拉一下代码
'拉的频率高一些,可以保证本地的版本库一直跟远端保持一样,这样冲突会较少'
git pull origin master
git push origin master # 把本地的mater分支推送到远端origin对应的地址
-如果推不上去,说明本地不是最新版本,一定下拉去一下,成了最新版本在去提交
ssh链接远程仓库,协同开发
1.本地仓库和远程仓库建立关系时,建立的是https的连接,公司一般用ssh的连接
1.1https和ssh的区别
-https:输入用户名密码,保存在凭据管理中
-ssh:配置公钥私钥
-生成公钥私钥,把公钥放到gitee上,配好远程仓库,以后直接提交,不需要输入任何秘钥的东西
1.2ssh的配置步骤
-按照这个步骤操作
https://gitee.com/help/articles/4181
-cmd命令行中,一路回车,会在(C:\Users\A-1/.ssh/id_ed25519)文件夹中下生成公钥和私钥
ssh-keygen -t ed25519 -C "1091434345@qq.com"
-生成一个公钥,一个私钥
id_ed25519 #私钥,千万不能泄露
id_ed25519.pub #公钥,可以给任何人
-将公钥配置在gitee的配置文件中,至此,你的机器和gitee互相上传下载代码,都不需要认证了
2.以后使用就不需要密码了
2.1删掉origin对应的https的路径
git remote remove origin
2.2查看一下是不是没有了
git remote
2.3配置成ssh的地址
git remote add origin git@gitee.com:liuqingzheng/luffy_api.git
2.4可以直接拉去,提交代码即可
git pull origin master
git push origin master
协同开发
1.在企业中工作时,都是多人共同开发同一个项目
1.1写完底层代码后,提交到远程仓库
1.2将代码clone到本地
1.3在pycharm中找一个位置:git bash here(cmd)
git clone 远程地址
-本地能运行起项目来,没跑起来可能是依赖没装好,或者数据库链接不对
1.4写代码,提交到本地版本库,推到远端即可(推之前先pull一下)
2.多人协同开发项目
-项目创建者:本地搞好,远端搞好,推上去,把别人加为开发者
-协同开发者:远程账号、密码,登录进去就能看到这个项目
线下分支冲突解决
多人同一分支开发出现冲突
1.冲突原因
1.1别人跟你改了同样的代码,但他先提交到远程仓库
-你要提交时,提交不上去,要先拉取下来,因为改了相同的代码冲突
1.2冲突的样式
<<<<<<< HEAD
print('lqz')
======= # 上面是你的代码,下面是别人代码
print('lqz is handsome')
>>>>>>> f67f73948d175b186cd5f1319d7602fe004e285c # 版本号
2.解决冲突
2.1修改代码到不报错
-代码没有关联时,就都留着
-代码有关联时,删掉自己的或别人的代码,尽量删掉自己的,如果要删掉别人的,要和别人说
2.2重新提交到版本库,推到远端
分支合并出现冲突
1.冲突原因
-开发分支在开发的时候,主分支又提交了版本,而你之后提交的开发分支代码又与其相冲突
2.制造冲突
1.1新建dev分支,切换过去,在将dev提交到版本库
git branch dev
git checkout dev
git commit -m '创建了dev.py'
1.2dev分支修改home/views.py 最后一行
# 在views.py最后一行加入注释
git add .
git commit -m '最后一行加入注释'
1.3切回到master分支,修改dev.py 第一行和home/views.py 最后一行,提交到版本库
git checkout master
# 在views.py最后一行加入注释
git add .
git commit -m 'master修改内容'
1.4合并代码时,就会出冲突
git merge dev
3.解决冲突
3.1修改代码到不报错
-代码没有关联时,就都留着
-代码有关联时,删掉自己的或别人的代码,尽量删掉自己的,如果要删掉别人的,要和别人说
3.2重新提交到版本库,推到远端
git add .
git commit -m '解决冲突'
线上分支合并(pr,mr)
1.建立远程分支
gitee上点击操作
2.把远程分支拉倒本地
git pull origin dev # 执行完后,已经拉下来了,但是看不到
git checkout dev # 切换到dev分支
3.本地dev分支增加代码
git add .
git commit -m '本地dev提交'
4.本地dev提交到远端
git push origin dev
5.远程分支合并(把dev合并到master)
-提交pull request的申请(pr,mr),之后跟你没关系了(合进去了,没有合进去)
-你的上级就可以看到这个pr,他审核通过,点合并,dev分支就合并到master分支了
pycharm操作git
-命令行操作git,编辑器pycharm可以在图形化界面操作操作git
-前面的git命令都可以在pycharm中点击实现
为开源项目贡献代码
1.看到开源项目,点击fork,你的仓库中就会有这个开源项目
2.在本地拉取你仓库中的fork的代码
3.在进行修改、增加,在提交到自己远程仓库
4.提交pr合并,如果作者同意,你的代码就会合并进去
git面试题
1.那么公司分支方案是什么样的?
-master、dev、bug三条分支
1.1master:主要用来发布版本,写好了某个版本的代码合并进去,不直接在master上面开发
1.2dev:开发分支,项目开发者都在dev分支上开发
1.3bug:bug分支,用来修改bug,发布小版本
2.使用fit开发,遇到过冲突吗?
-大部分在dev分支开发,会出现冲突
-分支合并也会冲突
-把拜吗拉下来,直接解决冲突,保留我代码或保留同事的代码
3.你知道git变基?
-分支合并:dev合并到master分支
-meege或rebase合并
-rebase会把多次提交合并成一个
4.git pull和git fetch的区别
-pull和fetch都是拉取代码
-pull = fetch + 合并
5.你知道git flow吗?
-git flow是git工作流,它是一个被人提出的分支方案
-我们用的就是master+dev+bug分支方案
6.使用git的操作流程
-普通开发者:git clone下来,写代码,git add .,git commit,git pull,git push
7.什么是gitee,github:pr,gitlab:mr?
-pr、mr只是不同叫法,是提交分支合并的请求
面试题补充
1 cgi fastcig WSGI uwsgi uWSGI
1.cgi:通用网关接口(Common Gateway Interface/CGI)是一种重要的互联网技术,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI描述了服务器和请求处理程序之间传输数据的一种标准。
-总结:一个标准,定义了客户端服务器之间如何传数据
2.fastcig:快速通用网关接口(Fast Common Gateway Interface/FastCGI)是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本
-总结:CGI的升级版
-常用的fastcgi软件:
Apache HTTP Server (部分) 常见项目架构LNMP
3.Nginx(主流):nginx是一个符合fastcgi协议的软件,处于浏览器和web程序之间,主要做请求转发和负载均衡,也可以称之为服务器中间件
Microsoft IIS:windows server
4.WSGI:Web服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。自从WSGI被开发出来以后,许多其它语言中也出现了类似接口
-总结:为Python定义的web服务器和web框架之间的接口标准
-wsgiref:性能很低,python实现的,django内置了,测试阶段用,上线不用
-uWSIG:性能高,c实现的
-gunicorn:python实现的
# uWSGI:符合wsgi协议的web服务器,上面标准的具体实现
# uwsgi:uWSGI服务器,自有协议
3 Apache
-Apache 公司
-Apache web服务器
-Apache 开源协议
-Kafka:apache顶级开源项目
-echars:原来是百度开发的,交给了apache孵化
redis
redis介绍
1.redis
是非关系型数据库,是内存存储,所以存储速度非常快,可以持久化(将数据从内存同步到硬盘),数据类型丰富(5大数据类型:字符串,列表,哈希(字典),集合,有序集合),key-value形式存储(没有表结构,相当于字典)
'nosql指非关系型数据库--1.不限于SQL--2.没有SQL'
2.redis为什么这么快
2.1高性能的网络模型:IO多路复用epoll模型,承载了非常高的并发量
2.2纯内存操作,避免了很多IO
2.3单线程操作,避免了线程间切换的消耗
-6.x之前是单线程、单进程
6.x之后用的是多线程架构,数据操作还是使用单线程,别的线程做数据持久化、其他操作
3redis应用场景
3.1当缓存数据库使用,接口缓存,提高接口响应速度
-请求进到视图---》去redis[内存]----》取json格式字符串---》返回给前端
# django
-请求进到视图---》去数据查询[多表查询,去硬盘取数据:速度慢]----》转成json格式字符串---》返回给前端
3.2做计数器:单线程,不存在并发安全问题
-统计网站访问量
3.3去重操作:集合
3.4排行榜:有序集合
3.5布隆过滤器
3.6抽奖
3.7消息队列
redis安装
1.redis是使用c语言写的开源软件,c语言是编译型语言,要在操作系统运行,要编译成可执行文件,由于采用了IO多复用的epoll模型,所以它不支持windows,只支持linux操作系统支持epoll
1.1微软官方修改了,编译成可执行的安装包,缺点是版本没有更新
2.官网
https://redis.io/
2.1下载完是源代码
c语言源码 :https://redis.io/download/#redis-stack-downloads
-
3.中文网
http://redis.cn/download.html
-最新只到5.x
4.win版本下载地址
4.1最新5.x版本
https://github.com/tporadowski/redis/releases/
4.2下载完一路下一步即可,具体可参照
https://www.cnblogs.com/liuqingzheng/p/9831331.html
5.win装完会有redis服务
-启动服务,手动停止
5.1客户端链接
redis-cli -h 127.0.0.1 -p 6379
5.2简单命令
set name lqz
get name
ping
5.3停掉服务
-去win服务点关闭
-客户端关闭
shutdown
6.redis服务端/客户端
redis-cli
-图形化工具
redis-destop-management
python操作redis
1.使用模块
pip3 install redis
2.使用
from redis import Redis
conn = Redis(host="localhost", port=6379)
# conn.set('name', 'xxx')
print(conn.get('name'))
conn.close()
redis连接池
POOL.py
import redis
pool = redis.ConnectionPool(max_connections=10, host='127.0.0.1', port=6379)
redis_dewmo.py
from threading import Thread
from POOL import pool
import time
import redis
# 创建一个连接池,保证它是单例,全局只有一个pool对象,使用模块导入方式实现单例
def get_name_redis():
# 没执行一次会从池中取一个连接,如果没有则等待
conn = redis.Redis(connection_pool=pool)
res = conn.get('name')
print(res)
conn.close()
for i in range(10):
t = Thread(target=get_name_redis)
t.start()
time.sleep(10)
redis字符串操作
- set(name, value, ex=None, px=None, nx=False, xx=False)
import redis
'''
ex,过期时间(秒)
px,过期时间(毫秒)
nx,如果设置为True,则只有name不存在时,当前set操作才执行,值存在,就修改不了,执行没效果
xx,如果设置为True,则只有name存在时,当前set操作才执行,值存在才能修改,值不存在,不会设置新值
'''
conn = redis.Redis()
conn.set('name', 'barry') # value只能是字符串或byte格式
conn.set('name', 'barry1', ex=3) # ex是限时销毁,3秒后数据就没了
conn.set('name', 'barry2', px=3) # ex是限时销毁,3秒后数据就没了
conn.set('name', 'barry3', nx=True) # 实现分布式锁
conn.set('name', 'barry4', xx=True)
conn.close()
- setnx(name, value)
本质就是set nx=True
conn = redis.Redis()
conn.setnx('hobby', '乒乓球')
conn.close()
-
psetex(name, time_ms, value)
本质就是set px 设置时间
conn = redis.Redis()
conn.psetex('name', 3000, 'barry')
conn.close()
-
mset(*args, **kwargs)
以字典格式批量设置
conn = redis.Redis()
conn.mset({'name': 'barry', 'age': 18})
conn.close()
-
get(name)
以bytes格式获取值
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
print(conn.get('name')) # b'barry'
conn.close()
-
mget(keys, *args)
以bytes格式批量获取获取值
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
res = conn.mget('name', 'age')
res1 = conn.mget(['name', 'age'])
print(res, res1) # [b'barry', b'18'] [b'barry', b'18']
conn.close()
-
getset(name, value)
先获取,在设置新的值
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
res = conn.getset('name', 'kevin')
print(res) # b'barry'
conn.close()
-
getrange(key, start, end)
以字节的格式按区间位置获取值
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
res = conn.getrange('name', 0, 1) # 前闭后闭区间
print(res) # b'ba'
conn.close()
-
setrange(name, offset, value)
从摸个起始位置开始替换字符串
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
conn.setrange('name', 1, 'xxx') # 从第1个位置开始替换字符串
print(conn.get('name')) # b'bxxxy'
conn.close()
-
setbit(name, offset, value)
以二进制,将某个位置的编码修改
conn = redis.Redis()
conn.setbit('name', 1, 0)
conn.close()
-
getbit(name, offset)
获取某个位置的比特位
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
res = conn.getbit('name', 1) # 获取第1个位置的比特位
print(res) # 1
conn.close()
-
bitcount(key, start=None, end=None)
获取从某个位置开始到某个位置结束有多少个比特位
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
res = conn.bitcount('name', 0, 3) # 3指的是3字符
print(res) # 14
conn.close()
-
strlen(name)
统计字节
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
print(conn.strlen('name')) # 5
conn.close()
-
incr(self, name, amount=1)
每执行一次就加1
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
print(conn.incr('age')) # 19
conn.close()
-
incrbyfloat(self, name, amount=1.0)
浮点型incr
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
print(conn.incrbyfloat('age')) # 20.0
conn.close()
-
decr(self, name, amount=1)
每执行一次就减1
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
print(conn.decr('age')) # 19
conn.close()
-
append(key, value)
在后面追加
conn = redis.Redis(encoding='utf8') # 将bytes转换为真正的值
conn.append('name', 'nb')
print(conn.get('name')) # b'barrynb'
conn.close()
- 常用:get set strlen
redis hash操作
- hset(name, key, value)
import redis
conn = redis.Redis()
conn.hset('info', 'name', 'barry') # 在info中以字典格式设置值
conn.hset('info', mapping={'age': 18, 'hobby': '篮球'}) # 在info中以字典格式设置多条值
conn.close()
-
hmset(name, mapping)
已经弃用的批量设置
conn = redis.Redis(decode_responses=True)
conn.hmset('aa', {'a': 'a', 'b': 'b'}) # 批量设置值
conn.close()
-
hget(name,key)
以bytes格式获取值
conn = redis.Redis()
res = conn.hget('info', 'name')
print(res) # b'barry'
conn.close()
-
hmget(name, keys, *args)
以bytes格式批量获取获取值
conn = redis.Redis()
res = conn.hmget('info', ['name', 'age'])
print(res) # [b'barry', b'18']
conn.close()
-
hgetall(name)、
获取所有(谨慎使用,可能会造成阻塞,尽量不要在生产代码中执行它)
conn = redis.Redis()
res = conn.hgetall('info')
print(res) # {b'name': b'barry', b'age': b'18'}
conn.close()
-
hlen(name)
获取其中有多少kv对
conn = redis.Redis()
res = conn.hlen('info')
print(res) # 2
conn.close()
-
hkeys(name)
获取所有的k中
conn = redis.Redis()
res = conn.hkeys('info')
print(res) # [b'name', b'age']
conn.close()
-
hvals(name)
获取所有的v值
conn = redis.Redis()
res = conn.hvals('info')
print(res) # [b'barry', b'18']
conn.close()
-
hexists(name, key)
判断是否存在
conn = redis.Redis()
res = conn.hexists('info', 'name')
print(res) # True
conn.close()
-
hdel(name,*keys)
删除指定键值对
conn = redis.Redis()
conn.hdel('info', 'age')
conn.close()
-
hincrby(name, key, amount=1)
每执行一次就加1
conn = redis.Redis()
conn.hincrby('info', 'age')
res = conn.hmget('info', ['age'])
print(res) # [b'19']
conn.close()
-
hincrbyfloat(name, key, amount=1.0)
浮点型hincrby
conn = redis.Redis()
conn.hincrbyfloat('info', 'age')
res = conn.hmget('info', ['age'])
print(res) # [b'19']
conn.close()
- hscan(name, cursor=0, match=None, count=None)
# 无序得设置值
conn = redis.Redis()
for i in range(100):
conn.hset('test_hash', 'key_%s' % i, '鸡蛋%s号' % i)
conn.close()
# 从第0条取10条值
res = conn.hscan('userinfo', cursor=0, count=10)
- hscan_iter(name, match=None, count=None)
# 获取10条数据,做成生成器
conn = redis.Redis()
res = conn.hscan_iter('test_hash', count=10)
print(res) # 生成器
for item in res:
print(item)
conn.close()
redis列表操作
-
lpush(name,values)
在列表左侧插入值
import redis
conn = redis.Redis(decode_responses=True)
conn.lpush('user', 'barry')
conn.close()
-
lpushx(name,value)
在name对应的list中添加元素,只有name已经存在时,值添加到列表的最左边
conn = redis.Redis(decode_responses=True)
conn.lpushx('user', 'kevin')
conn.close()
-
rpushx(name, value)
在列表右侧插入值
conn = redis.Redis(decode_responses=True)
conn.rpushx('user', 'barry1')
conn.close()
-
llen(name)
name对应的list元素的个数
conn = redis.Redis(decode_responses=True)
res = conn.llen('user')
print(res) # 5
conn.close()
-
linsert(name, where, refvalue, value))
在name对应的列表的某一个值前或后插入一个新值
conn = redis.Redis(decode_responses=True)
# 在barry后面插入tom
conn.linsert('user', where='after', refvalue='barry', value='tom')
# 在barry前面插入tom
conn.linsert('user', where='before', refvalue='barry', value='tom')
conn.close()
-
lset(name, index, value)
对name对应的list中的某一个索引位置重新赋值
conn = redis.Redis(decode_responses=True)
conn.lset('user', 0, 'xxx')
conn.close()
-
lrem(name, value, num)
在name对应的list中删除指定的值
conn = redis.Redis(decode_responses=True)
conn.lrem('user', 1, 'tom') # 删除从左往右的第一个tom
conn.close()
-
lpop(name)
在name对应的列表的左侧获取第一个元素并在列表中移除,返回值则是第一个元素
conn = redis.Redis(decode_responses=True)
res = conn.lpop('user')
print(res) # barry
conn.close()
-
lindex(name, index)
在name对应的列表中根据索引获取列表元素
conn = redis.Redis(decode_responses=True)
res = conn.lindex('user', 0)
print(res) # kevin
conn.close()
-
lrange(name, start, end)
在name对应的列表分片获取数据
conn = redis.Redis(decode_responses=True)
conn.lpushx('user', 'kevin')
res = conn.lrange('user', 0, 1)
print(res) # ['kevin', 'barry1']
conn.close()
-
ltrim(name, start, end)
在name对应的列表中移除没有在索引之间的值
conn = redis.Redis(decode_responses=True)
conn.ltrim('user', 0, 1)
conn.close()
-
rpoplpush(src, dst)
从一个列表取出最右边的元素,同时将其添加至另一个列表的最左边
conn = redis.Redis(decode_responses=True)
conn.rpoplpush('user', 'userinfo')
conn.close()
-
blpop(keys, timeout)
如果列表中没有值,会进入阻塞态,直到有值,在按照从左到右去pop对应列表的元素
conn = redis.Redis(decode_responses=True)
res = conn.blpop('user')
print(res) # ('user', 'kevin')
conn.close()
-
brpoplpush(src, dst, timeout=0)
从一个列表的右侧移除一个元素并将其添加到另一个列表的左侧
'''
src:取出并要移除元素的列表对应的name
dst:要插入元素的列表对应的name
timeout:当src对应的列表中没有数据时,阻塞等待其有数据的超时时间(秒),0 表示永远阻塞
'''
conn = redis.Redis(decode_responses=True)
conn.rpoplpush('user', 'userinfo')
conn.close()
通用操作(5大类型都支持)
-
delete(*names)
根据删除redis中的任意数据类型
conn = redis.Redis()
conn.delete('age', 'name') # 删除age、name
conn.close()
-
exists(name)
检测redis的name是否存在(0就是不存在,1就是存在)
conn = redis.Redis()
res = conn.exists('name')
print(res) # 0
conn.close()
-
keys(pattern='*')
根据模型获取redis的name
conn = redis.Redis()
res = conn.keys() # 返回所有的k
conn.close()
-
expire(name ,time)
为某个redis的某个name设置超时时间
conn = redis.Redis()
conn.expire('name', 3) # 3秒针过期
conn.close()
-
rename(src, dst)
对redis的name重命名为
conn = redis.Redis()
conn.delete('name', 'namexxx') # 将name修改为namexxx
conn.close()
-
move(name, db))
将redis的某个值移动到指定的db下
conn = redis.Redis()
conn.move('age', 2) # 将age移动到第2个库中
conn.close()
-
randomkey()
随机获取一个redis的name(不删除)
conn = redis.Redis()
res = conn.randomkey()
conn.close()
-
type(name)
获取name对应值的类型
conn = redis.Redis()
res = conn.type('age') # 获取age中值的数据类型
conn.close()
redis管道
1.redis数据库是否支持事务?
1.1redis事务机制可以保证一致性和隔离性,无法保证持久性,但是对于redis而言,本身是内存熟即可,所以持久化不是必须属性
-原子性需要自己进行检查,尽可能保证
1.2redis不像mysql一样,支持强事务,事务的四大特性不能全部满足,但是能满足一部分,通过redis的管道实现的
-redis本身不支持事务,但是可以通过管道,实现部分事务
1.3redis通过管道,来保证命令要么都成功,要么都失败,完成事务的一致性,但是管道只能用在单实例,集群环境中,不支持pipline
2.实例
import redis
conn = redis.Redis()
pipline = conn.pipeline(transaction=True)
pipline.decr('a', 2) # a减2
raise Exception('出错了')
pipline.incr('b', 2) # b加2
conn.close()
django中集成redis
1.方式1(直接使用)
1.1POOL.py
import redis
pool = redis.ConnectionPool(max_connections=10, host='127.0.0.1', port=6379)
1.2使用
from user.POOL import pool
import redis
def index(request):
conn = redis.Redis(connection_pool=pool)
conn.incr('page_view')
res = conn.get('page_view')
return HttpResponse(f'被你看了{res}次')
2.方式2
2.1在配置文件配置
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"max_connections": 100},
# "PASSWORD": "123", # 设置密码
}
}
}
2.2使用
from django_redis import get_redis_connection
def index(request):
conn = get_redis_connection(alias='default') # 每次从池中取一个衔接
conn.incr('page_view')
res = conn.get('page_view')
return HttpResponse(f'被你看了{res}次')
3.方式3(借助于django的缓存使用redis)
-如果配置文件中配置了CACHES,则django的缓存数据直接放在redis中
-可以直接使用cache.set设置值,可以设置过期时间
-使用cache.set获取值
-其优势在于可以直接缓存任意python对象,底层使用pickle实现的
from django.core.cache import cache
conn = cache.set('px', '10', 3) # 缓存到redis中,3秒后销毁
redis介绍安装和配置典型应用场景
# 介绍
开源:早起版本2w3千行 , c语言写的
基于键值对的存储系统:字典形式
多种数据结构:字符串,hash,列表,集合,有序集合
高性能,功能丰富
# Redis特性
1 快:10w ops(每秒10w读写),数据存在内存中,c语言实现,单线程模型
2 化:rdb和aof ,两种持久化方式
3 数据结构:
5大数据结构
BitMaps位图:布隆过滤器 本质是 字符串
HyperLogLog:超小内存唯一值计数,12kb HyperLogLog 本质是 字符串
GEO:地理信息定位 本质是有序集合
4 种编程语言:基于tcp通信协议,各大编程语言都支持
5 功能丰富:发布订阅(消息) Lua脚本,事务(pipeline)
6 简单:源代码几万行,不依赖外部库
7 主从复制:主服务器和从服务器,主服务器可以同步到从服务器中
8 高可用和分布式:
2.8版本以后使用redis-sentinel支持高可用
3.0版本以后支持分布式
# Redis典型使用场景
缓存系统:使用最广泛的就是缓存
计数器:网站访问量,转发量,评论数(文章转发,商品销量,单线程模型,不会出现并发问题)
消息队列:发布订阅,阻塞队列实现(简单的分布式,blpop:阻塞队列,生产者消费者)
排行榜:有序集合(阅读排行,点赞排行,推荐(销量高的,推荐))
社交网络:集合 ,很多特效跟社交网络匹配,粉丝数,关注数
实时系统:垃圾邮件处理系统,布隆过滤器
地理位置信息:GEO,附近的人。。。
redis的linux安装和卸载
#下载
wget http://download.redis.io/releases/redis-5.0.7.tar.gz
#解压
tar -xzf redis-5.0.7.tar.gz
#建立软连接
ln -s redis-5.0.7 redis
cd redis
make&&make install
# make:编译 之前只是编译,没有安装,所以做了软连接
# make install 安装到了 /usr/local/bin
#在src目录下可以看到
#redis-server--->redis服务器
#redis-cli---》redis命令行客户端
#redis-benchmark---》redis性能测试工具
#redis-check-aof--->aof文件修复工具
#redis-check-dump---》rdb文件检查工具
#redis-sentinel---》sentinel服务器,哨兵
####卸载redis
# 1、查看redis进程;
ps aux|grep redis
# 2、kill掉进程;
kill 进程id
# 3、进入到redis目录
cd /usr/local/
# 4、删除redis对应的文件
rm -f /usr/local/redis/bin/redis*
rm -f /usr/local/bin/redis*
# 5、删除对应的文件
rm -rf redis
三种启动方式
#1 最简启动
redis-server
ps -aux|grep redis #查看进程
netstat -antpl|grep redis #查看端口
netstat -nlp |grep 6379
redis-cli -h ip -p port ping #命令查看
# 2 动态参数启动
redis-serve --port 6380 #启动,监听6380端口
# 3 配置文件启动
#查看一下默认注释,把#和空格去掉
cat redis.conf|grep -v "#" |grep -v "^$"
#重定向到另一个文件
cat redis.conf|grep -v "#" |grep -v "^$" >redis-6382.conf
'''
daemonize yes #是否以守护进程启动
pidfile /var/run/redis.pid #进程号的位置,删除
port 6379 #端口号
dir /opt/soft/redis/data #工作目录
logfile 6379.log #日志位置
#其他全删掉
'''
#在redis目录下新建data目录,用来存放数据
#启动redis
redis-server /root/redis/redis.conf
#查看进程
ps -ef |grep redis-server |grep 6379
#查看日志
cd data
cat 6379.log
客户端连接
# redis-cli -h 地址 -p 端口
# 查看所有配置
config get * # 107对
port # 端口号
daemonize
logfile
dbfilename
requirepass # 设置密码
maxmemory # 最大占用内存,0 不限制
databases # 16 个库
maxclients # 有多少个客户端可以连接
appendonly # 持久化
dir # 工作路径
slaveof # 复制谁,如果是从库,这个地方要写主库
bind
# 修改配置(直接改文件,重启)
# 修改配置(客户端直接修改) ---》临时改,当前进程如果不停,生效,重启就没了
CONFIG SET maxmemory 128M
CONFIG get maxmemory
CONFIG set requirepass 123456
# 永久生效,同步到配置文件中
CONFIG REWRITE
# 直接客户端链接,修改配置信息,写到配置文件中
-如果以root用户启动redis,CONFIG REWRITE功能,会导致黑客提权,直接登录到系统中,放挖矿病毒
-避免:
-端口跑在别的上,不是6379
-redis密码设置尽量复制
-不用root用户启动redis
-启动redis的用户,不允许登录系统ssh链接
# redis返回值
状态回复:ping---》PONG
错误回复:hget hello field ---》(error)WRONGTYPE Operation against
整数回复:incr hello---》(integer) 1
字符串回复:get hello---》"world"
多行字符串回复:mget hello foo---》"world" "bar"
redis通用命令
# 力扣
# 大 O 表示法,衡量算法优劣的标准
-时间复杂度
-空间复杂度
-O(1) 最好的,只需要一次就能拿到结果
-O(log n) n的一半
-O(n) 循环n次
-O(n2) 两层for循环
# 1-keys
# 打印出所有key
keys *
# 打印出所有以he开头的key
keys he*
# 打印出所有以he开头,第三个字母是h到l的范围
keys he[h-l]
# 三位长度,以he开头,?表示任意一位
keys he?
# keys命令一般不在生产环境中使用,生产环境key很多,时间复杂度为o(n),用scan命令
# 2-dbsize 计算key的总数
dbsize # redis内置了计数器,插入删除值该计数器会更改,所以可以在生产环境使用,时间复杂度是o(1)
# 3-exists key 时间复杂度o(1)
# 设置a
set a b
# 查看a是否存在
exists a
(integer) 1
# 存在返回1 不存在返回0
# 4-del key 时间复杂度o(1)
del name # 删除成功返回1,key不存在返回0
# 5-expire key seconds 时间复杂度o(1)
expire name 3 # 3s 过期
ttl name # 查看name还有多长时间过期
persist name #去掉name的过期时间
# 6-type key 时间复杂度o(1)
type name # 查看name类型,返回string
# 7 其他
info命令:内存,cpu,主从相关 ,redis监控
- redis-cli -a 123456 info --->返回字符串 subprocess
client list # 正在连接的会话
client kill ip:端口
dbsize # 总共有多少个key
flushall # 清空所有
flushdb # 只清空当前库
select # 数字 选择某个库 总共16个库
monitor # 记录操作日志,夯住
redis 字符串命令
# 1---基本使用get,set,del
get name # 时间复杂度 o(1)
set name lqz # 时间复杂度 o(1)
del name # 时间复杂度 o(1)
# 2---其他使用incr,decr,incrby,decrby
incr age # 对age这个key的value值自增1
decr age # 对age这个key的value值自减1
incrby age 10 # 对age这个key的value值增加10
decrby age 10 # 对age这个key的value值减10
# 统计网站访问量(单线程无竞争,天然适合做计数器)
# 分布式id生成(多个机器同时并发着生成,不会重复)
# 3---set,setnx,setxx
set name lqz # 不管key是否存在,都设置
setnx name lqz # key不存在时才设置(新增操作)
set name lqz nx # 同上
set name lqz xx # key存在,才设置(更新操作)
# 4---mget mset
mget key1 key2 key3 #批量获取key1,key2.。。时间复杂度o(n)
mset key1 value1 key2 value2 key3 value3 #批量设置时间复杂度o(n)
# n次get和mget的区别
# n次get时间=n次命令时间+n次网络时间
# mget时间=1次网络时间+n次命令时间
# 5---其他:getset,append,strlen
getset name lqznb # 设置新值并返回旧值 时间复杂度o(1)
append name 666 # 将value追加到旧的value 时间复杂度o(1)
strlen name # 计算字符串长度(注意中文) 时间复杂度o(1)
# 6---其他:incrybyfloat,getrange,setrange
increbyfloat age 3.5 # 为age自增3.5,传负值表示自减 时间复杂度o(1)
getrange key start end # 获取字符串制定下标所有的值 时间复杂度o(1)
setrange key index value # 从指定index开始设置value值 时间复杂度o(1)
# 作用:
缓存,计数器
redis 列表命令
# 1 插入操作
# rpush 从右侧插入
rpush key value1 value2 ...valueN #时间复杂度为o(1~n)
# lpush 从左侧插入
# linsert
linsert key before|after value newValue # 从元素value的前或后插入newValue 时间复杂度o(n) ,需要遍历列表
linsert listkey before b java
linsert listkey after b php
# 2 删除操作
lpop key # 从列表左侧弹出一个item 时间复杂度o(1)
rpop key # 从列表右侧弹出一个item 时间复杂度o(1)
lrem key count value
#根据count值,从列表中删除所有value相同的项 时间复杂度o(n)
1 count>0 从左到右,删除最多count个value相等的项
2 count<0 从右向左,删除最多 Math.abs(count)个value相等的项
3 count=0 删除所有value相等的项
lrem listkey 0 a #删除列表中所有值a
lrem listkey -1 c #从右侧删除1个c
ltrim key start end #按照索引范围修剪列表 o(n)
ltrim listkey 1 4 #只保留下表1--4的元素
# 3 查询操作
lrange key start end # 包含end获取列表指定索引范围所有item o(n)
lrange listkey 0 2
lrange listkey 1 -1 #获取第一个位置到倒数第一个位置的元素
lindex key index # 获取列表指定索引的item o(n)
lindex listkey 0
lindex listkey -1
llen key # 获取列表长度
# 4 修改操作
lset key index newValue #设置列表指定索引值为newValue o(n)
lset listkey 2 ppp #把第二个位置设为ppp
# 5 其他
blpop key timeout # lpop的阻塞版,timeout是阻塞超时时间,timeout=0为拥有不阻塞 o(1)
brpop key timeout # rpop的阻塞版,timeout是阻塞超时时间,timeout=0为拥有不阻塞 o(1)
# 要实现栈的功能
lpush+lpop
#实现队列功能
lpush+rpop
#固定大小的列表
lpush+ltrim
#消息队列
lpush+brpop
# 作用,功能
实现timeLine功能,时间轴,微博关注的人,按时间轴排列,在列表中放入关注人的微博的即可
redis 字典命令
# 1---hget,hset,hdel
hget key field #获取hash key对应的field的value 时间复杂度为 o(1)
hset key field value # 设置hash key对应的field的value值 时间复杂度为 o(1)
hdel key field # 删除hash key对应的field的值 时间复杂度为 o(1)
# 测试
hset user:1:info age 23
hget user:1:info ag
hset user:1:info name lqz
hgetall user:1:info
hdel user:1:info age
# 2---hexists,hlen
hexists key field # 判断hash key 是否存在field 时间复杂度为 o(1)
hlen key # 获取hash key field的数量 时间复杂度为 o(1)
hexists user:1:info name
hlen user:1:info # 返回数量
# 3---hmget,hmset
hmget key field1 field2 ...fieldN # 批量获取hash key 的一批field对应的值 时间复杂度是o(n)
hmset key field1 value1 field2 value2 # 批量设置hash key的一批field value 时间复杂度是o(n)
# 4--hgetall,hvals,hkeys
hgetall key # 返回hash key 对应的所有field和value 时间复杂度是o(n)
hvals key # 返回hash key 对应的所有field的value 时间复杂度是o(n)
hkeys key # 返回hash key对应的所有field 时间复杂度是o(n)
# 小心使用hgetall
# 计算网站每个用户主页的访问量
hincrby user:1:info pageview count
# 5 其他操作 hsetnx,hincrby,hincrbyfloat
hsetnx key field value # 设置hash key对应field的value(如果field已存在,则失败),时间复杂度o(1)
hincrby key field intCounter # hash key 对英的field的value自增intCounter 时间复杂度o(1)
hincrbyfloat key field floatCounter # hincrby 浮点数 时间复杂度o(1)
# 作用
-缓存,缓存用户信息,数据
-计数器
redis 集合
# 无序,无重复,集合间操作(交叉并补)
# 常用api
sadd key element # 向集合key添加element(如果element存在,添加失败) o(1)
srem key element # 从集合中的element移除掉 o(1)
scard key # 计算集合大小
sismember key element # 判断element是否在集合中
srandmember key count # 从集合中随机取出count个元素,不会破坏集合中的元素
spop key # 从集合中随机弹出一个元素
smembers key # 获取集合中所有元素 ,无序,小心使用,会阻塞住
sdiff user:1:follow user:2:follow # 计算user:1:follow和user:2:follow的差集
sinter user:1:follow user:2:follow # 计算user:1:follow和user:2:follow的交集
sunion user:1:follow user:2:follow # 计算user:1:follow和user:2:follow的并集
sdiff|sinter|suion + store destkey... # 将差集,交集,并集结果保存在destkey集合中
# 作用
抽奖系统 :通过spop来弹出用户的id,活动取消,直接删除
点赞,点踩,喜欢等,用户如果点了赞,就把用户id放到该条记录的集合中
标签:给用户/文章等添加标签,sadd user:1:tags 标签1 标签2 标签3
给标签添加用户,关注该标签的人有哪些
共同好友:集合间的操作
redis 有序集合
-不重复,有个分值字段,保证集合有顺序
-操作
zadd key score element # score可以重复,可以多个同时添加,element不能重复 o(logN)
zrem key element # 删除元素,可以多个同时删除 o(1)
zscore key element # 获取元素的分数 o(1)
zincrby key increScore element # 增加或减少元素的分数 o(1)
zcard key # 返回元素总个数 o(1)
zrank key element # 返回element元素的排名(从小到大排)
zrange key 0 -1 # 返回排名,不带分数 o(log(n)+m) n是元素个数,m是要获取的值
zrange player:rank 0 -1 withscores # 返回排名,带分数
zrangebyscore key minScore maxScore # 返回指定分数范围内的升序元素 o(log(n)+m) n是元素个数,m是要获取的值
zrangebyscore user:1:ranking 90 210 withscores # 获取90分到210分的元素
zcount key minScore maxScore # 返回有序集合内在指定分数范围内的个数 o(log(n)+m)
zremrangebyrank key start end # 删除指定排名内的升序元素 o(log(n)+m)
zremrangebyrank user:1:rangking 1 2 # 删除升序排名中1到2的元素
zremrangebyscore key minScore maxScore # 删除指定分数内的升序元素 o(log(n)+m)
zremrangebyscore user:1:ranking 90 210 # 删除分数90到210之间的元素
zrevrank # 从高到低排序
zrevrange # 从高到低排序取一定范围
zrevrangebyscore # 返回指定分数范围内的降序元素
zinterstore # 对两个有序集合交集
zunionstore # 对两个有序集合求并集
# 作用
排行榜:音乐排行榜,销售榜,关注榜,游戏排行榜
高级用法之慢查询
# 5大数据类型,O(n), 命令执行时间很长,redis 命令操作 单线程架构 ,阻塞
-单线程架构:并发操作不需要锁
-mysql:行锁,表锁,并发操作数据错乱的问题
# 慢查询命令 记录下来,以备后期查看,排查,redis的配置,用来记录慢查询,如果符合这个配置,这条命令就会被记录
# 命令操作生命周期
-客户端到服务端的网络时间
-执行命令时间 # 我们指的慢查询指的是这里。设定一个时间,如果超过了这个时间,我们就认为是慢查询
-服务端返回客户端时间
# 配置:两个配置项
config get slowlog-max-len=128 # 慢查询队列是128
config get slowly-log-slower-than=10000 # 微秒,超过这个时间,我们就记录
# 动态配置
config set slowlog-log-slower-than 0 # 记录所有
config set slowlog-max-len 128
# 持久化到本地配置文件
config rewrite
# 查看慢命令队列
slowlog get [n] # 获取慢查询队列
'''
日志由4个属性组成:
1)日志的标识id
2)发生的时间戳
3)命令耗时
4)执行的命令和参数
'''
slowlog len # 获取慢查询队列长度
slowlog reset # 清空慢查询队列
# 作用
-后期发现redis速度非常慢,开启慢日志,过一会,查看一下慢命令,取出慢命令,分析这个命令是不是必须要执行,把这个慢命令换一种方式实现,从而加快操作速度
pipline与事务
# redis 本身不支持事务,通过管道实现部分事务
Redis的pipeline(管道)功能在命令行中没有,但redis是支持pipeline的,而且在各个语言版的client中都有相应的实现
将一批命令,批量打包,在redis服务端批量计算(执行),然后把结果批量返回
1次pipeline(n条命令)=1次网络时间+n次命令时间
# python代码实现
import redis
pool = redis.ConnectionPool(host='127.0.0.1', port=6379)
r = redis.Redis(connection_pool=pool)
# 创建pipeline
pipe = r.pipeline(transaction=True)
# 开启事务
pipe.multi()
pipe.set('name', 'lqz')
# 其他代码,可能出异常
pipe.set('role', 'nb')
pipe.execute() # 一次性执行命令,在这之前,如果处理异常,所有命令都不会执行,保证了一致性
# 原生事务操作
# 1 mutil 开启事务,放到管道中一次性执行
multi # 开启事务
set name lqz
set age 18
exec
# 原生redis使用管道
# 2 模拟事务
# 在开启事务之前,先watch,只有这个age,在整个过程中没有被改变,才能正常修改成功,如果在执行exec时发现,age被改了,就修失败
watch age
multi
decr age
exec
# 另一台机器
multi
decr age
exec # 先执行,上面的执行就会失败(乐观锁,被wathc的事务不会执行成功)
发布订阅
# 观察者模式:我们现在订阅了一个人,只要这个人发生变化,我们都能收到变化
发布者发布了消息,所有的订阅者都可以收到,就是生产者消费者模型(后订阅了,无法获取历史消息)
# 关注了某个明星,只要明星发布动态,大家都能收到
# 订阅了某个人的博客,只要它发布新博客,都能收到
# 订阅了某个商品的抢购提醒,当要开始抢购的时候,你收到短信通知
# redis实现发布订阅
# 1 发布消息
publish souhu:tv "hello world"
# 2 订阅消息
subscribe souhu:tv
# 发布订阅和消息队列区别
-只要订阅了,所有人都能收到消息
-消息队列,只有一个人能抢到消息

bitmap位图
# 本质是字符串,可以操作某个比特位
# 比如字符串是 big,设置和获取某个比特位
set hello big # 放入key位hello 值为big的字符串 01100010 01101001 01100111
# 获取比特位
getbit hello 0 # 取位图的第0个位置,返回0
getbit hello 1 # 取位图的第1个位置,返回1 如上图
# 设置比特位
setbit hello 7 1
# 获取对应的字符串
get hello
# 获取指定范围内1 的个数 ,前闭后闭区间
bitcount key start到end # start和end单位为字节,注意按字节一个字节8个bit为
# 作用:做独立用户统计,日活用户统计,大数据量的日活统计,使用它会节约内存
-用户id 是数字,1,2,3....
-日活:每日的用户活跃数,用户登录后,把id号放到集合中
- 1 2 3 9999 字符串 9999四个字符 4*8 32个比特位,用户量越多,存储用户id用的空间就越大
-使用setbit设置 ,只要用户登录了,就在对应的位置设为1,没有登录就是0
setbit users 9999 1
setbit users 1 1
最终通过bitcount计算出总共有多少个1,就是多少个活跃用户
-抖音日活
-集合 : 用户id方式 3亿用户*32比特位
-位图 方式 3亿*1 个比特位
setbit users 10亿 1
# 做日活,使用集合和位图比较
1 使用set和Bitmap对比
2 1亿用户,5千万独立(1亿用户量,约5千万人访问,统计活跃用户数量)
数据类型 每个userid占用空间 需要存储用户量 全部内存量
set 32位(假设userid是整形,占32位) 5千万 32位*5千万=200MB
bitmap 1位 1亿 1位*1亿=12.5MB
假设有10万独立用户,使用位图还是占用12.5mb,使用set需要32位*1万=4MB
HyperLogLog
# HyperLogLog:本质还是字符串
基于HyperLogLog算法:极小的空间完成独立数量统计
# 用来统计某个值是否在其中,只能统计,不能取出来
# 集合可以取值,统计是否在其中,但是集合占的空间大
# 使用
pfadd key element # 向hyperloglog添加元素,可以同时添加多个
pfcount key # 计算hyperloglog的独立总数
# 类似于集合,去重,统计某个元素是否在其中,之前使用去重的地方,就可以使用它
# 独立用户统计,也可以使用HyperLogLog
-如果用户id不是数字,而是uuid无规律的形式 asdf-asdfw-dsdf-asdf
# 错误率,布隆过滤器本质是一样的,使用HyperLogLog算法
百万级别独立用户统计,百万条数据只占15k
错误率 0.81%
无法取出单条数据,只能统计个数
GEO
# GEO(地理信息定位):存储经纬度,计算两地距离,范围等
-抖音,附近的人
-美团,附近的美食
-交友软件,附近的美女
# 存储经纬度,通过经纬度,计算距离
-经纬度哪里来?前端(app,网页),前端获取用户授权,用户授权后,通过某个方法,可以直接获取到手机的位置,获取出来就是经纬度
-通过后台某个接口,把此时的经纬度,提交到后台,后台保存到redis的geo中
# 1 存数据
geoadd user:locations 116.28 39.55 1
geoadd user:locations 116.28 42.55 2
geoadd cities:locations 116.28 39.55 beijing # 把北京地理信息天津到cities:locations中
geoadd cities:locations 117.12 39.08 tianjin
geoadd cities:locations 114.29 38.02 shijiazhuang
geoadd cities:locations 118.01 39.38 tangshan
geoadd cities:locations 115.29 38.51 baoding
# 2 获取地理位置信息 ---->获取张三的位置
# geopos user:locations 2 --->经纬度----》开放的接口,通过经纬度获得具体的位置信息
geopos cities:locations beijing # 获取北京地理信息
# 3 获取两个地点的距离
geodist cities:locations beijing tianjin km
# 4 方圆100km内有哪些城市
# 改一个接口,交友类app,附近5公里的美女,每次都会把它自己查出来,不显示自己,只查别人
georadiusbymember cities:locations beijing 150 km
# geo 本质是zset类型
持久化
# redis redis的所有数据保存在内存中,对数据的更新将异步的保存到硬盘上,更新到硬盘上这个操作称之为持久化
# 常见的持久化方案
快照:某时某刻数据的一个完成备份
-mysql的Dump
-redis的RDB
写日志:任何操作记录日志,要恢复数据,只要把日志重新走一遍即可
-mysql的 Binlog
-Redis的 AOF
# redis 支持两种持久化方案
-rdb :快照,某一时刻完整备份
-aof: 日志,进行了操作就记录日志
-混合持久化:rdb+aof方案,后来加入的,为了快速恢复数据
rdb持久化方案
# 三种触发机制
-手动:save
-手动:bgsave
-配置文件:符合条件就触发
# 方式一:
-在客户端执行save,把此时内存中的数据,完整的备份到硬盘上,生成一个xx.rdb 文件,当redis服务器停止,再启动,会加载rdb文件数据到内存,达到快速恢复的效果
-sava 会阻塞住,导致其他命令执行不了
# 方式二:
-在客户端执行bgsave,异步备份,不会阻塞其他命令的执行
# 方式三:配置文件 ,用它比较多
save 900 1
save 300 10
save 60 10000
# 如果60s中改变了1w条数据,自动生成rdb
# 如果300s中改变了10条数据,自动生成rdb
# 如果900s中改变了1条数据,自动生成rdb
# 我的配置
save 900 20
save 300 10
save 60 5
dbfilename dump.rdb
# 客户端主动关闭服务端时,也会触发
# rdb 方案会存在数据丢失的情况,一般咱们用redis做缓存,可以使用这种方案,缓存丢失,影响不大,但如果对数据准确性要求比较高,应该使用aof方案
aof 方案
# 客户端每写入一条命令,都记录一条日志,放到日志文件中,如果出现宕机,可以将数据完全恢复
# AOF的三种策略
日志不是直接写到硬盘上,而是先放在缓冲区,缓冲区根据一些策略,写到硬盘上
always:redis–》写命令刷新的缓冲区—》每条命令fsync到硬盘—》AOF文件 # redis写入速度很快,每条日志都立马写到硬盘上,硬盘性能跟不上
everysec(默认值):redis——》写命令刷新的缓冲区—》每秒把缓冲区fsync到硬盘–》AOF文件 # 每s把日志写到硬盘上
no:redis——》写命令刷新的缓冲区—》操作系统决定,缓冲区fsync到硬盘–》AOF文件 # 操作系统决定
# AOF 重写策略
随着命令的逐步写入,并发量的变大, AOF文件会越来越大,通过AOF重写来解决该问题
本质就是把过期的,无用的,重复的,可以优化的命令,来优化
这样可以减少磁盘占用量,加速恢复速度
# 配置文件配置好,会自动开启aof重写,优化aof日志文件
auto-aof-rewrite-min-size # AOF文件到达某个尺寸,就触发aof重写
auto-aof-rewrite-percentage # AOF文件增长率,到达某个比率,就触发aof重写
# 最佳配置
appendonly yes # 将该选项设置为yes,打开,开启aof
appendfilename appendonly.aof # 文件保存的名字
appendfsync everysec # 采用第二种策略
no-appendfsync-on-rewrite yes # 在aof重写的时候,是否要做aof的append操作,因为aof重写消耗性能,磁盘消耗,正常aof写磁盘有一定的冲突,这段期间的数据,允许丢失
# 你们公司采用了什么持久化方案
-如果是缓存,就用rdb
-如果对数据准确性要求高,就用aof
-混合持久化:两个都开启
主从复制
# mysql 主从复制 ,原理 binlog,relaylog,io线程,sql线程
# redis 主从复制
# 主从复制出现原因
问题:机器故障;容量瓶颈;QPS瓶颈
解决问题:一主一从,一主多从,做读写分离,做数据副本
扩展数据性能
一个master可以有多个slave
一个slave只能有一个master
数据流向是单向的,从master到slave
# redis主从复制原理
1. 副本库通过slaveof 127.0.0.1 6379命令,连接主库,并发送SYNC给主库
2. 主库收到SYNC,会立即触发BGSAVE,后台保存RDB,发送给副本库
3. 副本库接收后会应用RDB快照
4. 主库会陆续将中间产生的新的操作,保存并发送给副本库
5. 到此,我们主复制集就正常工作了
6. 再此以后,主库只要发生新的操作,都会以命令传播的形式自动发送给副本库.
7. 所有复制相关信息,从info信息中都可以查到.即使重启任何节点,他的主从关系依然都在.
8. 如果发生主从关系断开时,从库数据没有任何损坏,在下次重连之后,从库发送PSYNC给主库
9. 主库只会将从库缺失部分的数据同步给从库应用,达到快速恢复主从的目的
主从复制搭建
# 方式一:
-两台机器,启动两个redis进程
# 第一台机器配置
daemonize yes
port 6379
dir "/root/redis/data"
logfile "6379.log"
save 900 20
save 300 10
save 60 5
dbfilename dump.rdb
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
no-appendfsync-on-rewrite yes
# 第二台机器配置
daemonize yes
port 6380
dir "/root/redis/data2"
logfile "6380.log"
save 900 20
save 300 10
save 60 5
dbfilename dump.rdb
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
no-appendfsync-on-rewrite yes
# 启动两个redis-server服务
redis-server ./redis_6379.conf
redis-server ./redis_6380.conf
ps aux |grep redis
# 在6380库上执行
slaveof 127.0.0.1 6379
以后6380库就是6379的从库了,不能再写数据了 # (error)READONLY You can't write against a read only replica.
# info 命令查看主从关系
# 一主多从也是同理
配置文件搭建方式
# 在从库配置文件加入
slaveof 127.0.0.1 6379
slave-read-only yes
# 启动三个redis服务
-6379 主
-6380 从
-6381 从
# 停掉一个从库,主从关系还在
# 停掉主库,就不能写数据了
补充
# 1 redis高级---》发布订阅---》观察者模式(23种设计模式:单例(5种),工厂,代理模式,装饰器模式)
https://www.liuqingzheng.top/python/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E9%AB%98%E9%98%B6/19-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E9%AB%98%E7%BA%A7%E5%AE%9E%E6%88%98%E4%B9%8B%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/
-订阅者只要订阅了,发布者发布消息,所有订阅者都会收到 (rabbitmq)
-跟消息队列的区别:只能有一个消费者收到消息
# 2 redis高级---》pipline,事务
-事务4大特性,事务隔离级别,mysql 5.7 默认隔离级别是多少
-https://www.cnblogs.com/liuqingzheng/p/16480047.html
-通过pipline来部分实现
-python客户端操作
-watch 乐观锁
-乐观锁:乐观的认为,我的本次更改,不会有别人修改,如果有别人修改了这个值,我的修改就不会成功,我的修改不一定成功
-django中使用乐观锁: 开启事务,查询库存 8,大于0,可以秒杀 ---》生成订单(订单表插入一条记录),库存-1,res=filter(kc=8).update(kc=7) --》 提交事务
-悲观锁:悲观的任务,我的本次更改,会有别人修改,在开始就加锁,所有人都改不了了,修改完释放锁,能保证我的修改一定成功
- django中使用悲观锁:.select_for_update() 提交事务
-redis 悲观锁,使用分布式锁实现
哨兵
# 主从:一主两从,如果一个从库挂掉,不影响整体对外服务的,但如果主库挂掉,不能再写入数据了,整个服务就有问题
# 我们需要保证,高可用:服务的高度可用性 99.999% 可用性,使用redis的哨兵(sentinel),保证高可用
# 主从复制存在的问题
1 主从复制,主节点发生故障,需要做故障转移,可以手动转移:让其中一个slave变成master 哨兵解决
2 主从复制,只能主写数据,所以写能力和存储能力有限 集群解决
# 哨兵原理
可以做故障判断,故障转移,通知客户端(其实是一个进程),客户端直接连接sentinel的地址
1 多个sentinel发现并确认master有问题
2 选举触一个sentinel作为领导
3 选取一个slave作为新的master
4 通知其余slave成为新的master的slave
5 通知客户端主从变化
6 等待老的master复活成为新master的slave
# 搭建哨兵
-三个哨兵
-3个进程
-一主两从
-3个redis服务
搭建步骤
# 第一步:搭建一主两从
# 第一个是主配置文件
daemonize yes
pidfile /var/run/redis.pid
port 6379
dir "/root/redis/data"
logfile 6379.log
# 第二个是从配置文件
daemonize yes
pidfile /var/run/redis2.pid
port 6378
dir "/root/redis/data1"
logfile 6378.log
slaveof 127.0.0.1 6379
slave-read-only yes
# 第三个是从配置文件
daemonize yes
pidfile /var/run/redis3.pid
port 6377
dir "/root/redis/data2"
logfile 6377.log
slaveof 127.0.0.1 6379
slave-read-only yes
# 把三个redis服务都启动起来
redis-server ./redis_6379.conf
redis-server ./redis_6378.conf
redis-server ./redis_6377.conf
# 第二步:2 搭建哨兵
# sentinel.conf这个文件
# 【把哨兵也当成一个redis服务】
创建三个配置文件分别叫sentinel_26379.conf sentinel_26378.conf sentinel_26377.conf
# 当前路径下创建 data1 data2 data3 个文件夹
#内容如下(需要修改端口,文件地址日志文件名字)
vim sentinel_26379.conf
port 26379
daemonize yes
dir /root/redis/data
protected-mode no
bind 0.0.0.0
logfile "redis_sentinel.log"
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
vim sentinel_26378.conf
port 26378
daemonize yes
dir /root/redis/data1
protected-mode no
bind 0.0.0.0
logfile "redis_sentinel1.log"
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
vim sentinel_26377.conf
port 26377
daemonize yes
dir /root/redis/data2
protected-mode no
bind 0.0.0.0
logfile "redis_sentinel2.log"
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
#启动三个哨兵
redis-sentinel ./sentinel_26379.conf
redis-sentinel ./sentinel_26378.conf
redis-sentinel ./sentinel_26377.conf
# 第三步:登录哨兵,查看信息
redis-cli -p 26379
info # 查看哨兵信息
redis-cli -p 6379
info # 查看主从信息
# 第四步:故障演示
主库停掉,哨兵会自动做故障切换,把6378作为了主库,6377还是从库
把原来6379启动,现在它就变成了从库(不是主库了)
# 解释
sentinel monitor mymaster 127.0.0.1 6379 2 # 服务名字叫 mymaster 2个哨兵觉得主库挂掉了,主库就真的挂掉了,就开始做故障切换
sentinel down-after-milliseconds mymaster 30000 # 这个配置项指定了需要多少失效时间,一个master才会被这个sentinel主观地认为是不可用的。 单位是毫秒,默认为30秒
sentinel parallel-syncs mymaster 1 # 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
sentinel failover-timeout mymaster 180000
# failover-timeout 可以用在以下这些方面:
1. 同一个sentinel对同一个master两次failover之间的间隔时间。
2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
3.当想要取消一个正在进行的failover所需要的时间。
4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了。
python操作哨兵
# python操作哨兵,代码中需要单独写了,跟之前不一样了
import redis
from redis.sentinel import Sentinel
# 连接哨兵服务器(主机名也可以用域名)
# 10.0.0.101:26379
sentinel = Sentinel([('127.0.0.1', 26379),
('127.0.0.1', 26378),
('127.0.0.1', 26377)],
socket_timeout=5)
print(sentinel)
# 获取主服务器地址
master = sentinel.discover_master('mymaster')
print(master)
# 获取从服务器地址
slave = sentinel.discover_slaves('mymaster')
print(slave)
# 读写分离
# 获取主服务器进行写入
# master = sentinel.master_for('mymaster', socket_timeout=0.5)
# w_ret = master.set('foo', 'bar')
# slave = sentinel.slave_for('mymaster', socket_timeout=0.5)
# r_ret = slave.get('foo')
# print(r_ret)
集群
# 存在问题
1 并发量:单机redis qps为10w/s,但是我们可能需要百万级别的并发量
2 数据量:机器内存16g--256g,如果存500g数据呢?
# 解决:加机器,分布式
redis cluster 在2015年的 3.0 版本加入了,满足分布式的需求
# 数据分布(分布式数据库)
# 假设全量的数据非常大500g,单机已经无法满足,我们需要进行分区,分到若干个子集中---》数据分片
# 分区,分片方式
- 顺序分区
-原理:100个数据分到3个节点上 1--33第一个节点;34--66第二个节点;67--100第三个节点(很多关系型数据库使用此种方式)
-mysql 分库分表
- hash分区:原理:hash分区: 节点取余 ,假设3台机器, hash(key)%3,落到不同节点上
-节点取余分区
-一致性哈希分区
-虚拟槽分区
# redis使用的分区方案:-redis使用hash分区中的虚拟槽分区,总共有16384个槽
-原理:5个节点,把16384个槽平均分配到每个节点,客户端会把数据发送给任意一个节点,通过CRC16对key进行哈希对16383进行取余,算出当前key属于哪部分槽,属于哪个节点,每个节点都会记录是不是负责这部分槽,如果是负责的,进行保存,如果槽不在自己范围内,redis cluster是共享消息的模式,它知道哪个节点负责哪些槽,返回结果,让客户端找对应的节点去存服务端管理节点,槽,关系
# 为什么redis 有16384个槽
-一般redis集群节点数,不会超过1千多个,16384就够了
-如果槽过多,通信效率也会受影响
-综合权衡完,使用16384个槽
# redis集群搭建---》流程---》了解
节点(6台机器,一主已从,3个节点),meet(每个节点需要相互感知),指派槽(16384分配给3台机器),复制(一主一从),高可用(主库挂掉,从库会顶上来)
搭建步骤 (搭建一个6台机器,3个节点的集群,另外3台机器是从库,启动两台(一主一从),扩容成4个节点,缩容成3个节点)
# 第一步:准备6台机器
# 配置文件解释
cluster-enabled yes # 开启cluster
cluster-node-timeout 15000 # 故障转移,超时时间 15s
cluster-config-file nodes-${port}.conf # 给cluster节点增加一个自己的配置文件
cluster-require-full-coverage yes #只要集群中有一个故障了,整个就不对外提供服务了,这个实际不合理,假设有50个节点,一个节点故障了,所有不提供服务了;,需要设置成no
-准备6个配置文件
vim redis-7000.conf
port 7000
daemonize yes
dir "/root/redis/data/"
logfile "7000.log"
dbfilename "dump-7000.rdb"
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-require-full-coverage yes
# 快速生成其他配置6个排位置文件
sed 's/7000/7001/g' redis-7000.conf > redis-7001.conf
sed 's/7000/7002/g' redis-7000.conf > redis-7002.conf
sed 's/7000/7003/g' redis-7000.conf > redis-7003.conf
sed 's/7000/7004/g' redis-7000.conf > redis-7004.conf
sed 's/7000/7005/g' redis-7000.conf > redis-7005.conf
# 启动6台机器
redis-server ./redis-7000.conf
redis-server ./redis-7001.conf
redis-server ./redis-7002.conf
redis-server ./redis-7003.conf
redis-server ./redis-7004.conf
redis-server ./redis-7005.conf
ps -ef |grep redis
# 第二步:相互感知,分配主从,分配槽
# meet,分配槽,指定主从 1 的意思是,每个节点都是一主一从 ,自动组件主从
redis-cli --cluster create --cluster-replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
yes
# 第三步:主从关系和集群关系
# 7000 主 7004从
# 7001 主 7005 从
# 7002 主 7003从
# 3个命令
cluster nodes # 看节点信息
cluster slots # 槽的信息
cluster info # 整体信息
# 链接服务的
redic-cli -c # 集群模式链接,设置获取值,如果不在当前节点,会自动转过去,并完成数据操作
# 扩容,8台机器,4个节点的集群
-启动两台机器,加入到集群中(meet),两台机器做主从,这个节点分配槽
# 启动两台集群:一个主,一个从
-第一步启动两台机器
sed 's/7000/7006/g' redis-7000.conf > redis-7006.conf
sed 's/7000/7007/g' redis-7000.conf > redis-7007.conf
-第二步:启动新增的两台机器
redis-server ./redis-7006.conf
redis-server ./redis-7007.conf
-第三步:两台机器加入到集群中去
-这两台机器加入到机器中(add-node)
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000
redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7000
-第四步:让7007作为7006的从
redis-cli -p 7007 cluster replicate fb83d0f5c350ccf4c2e067f1b2081e236a266b15
-第五步:迁移槽:从每台机器均匀的移动一部分槽给新的机器
redis-cli --cluster reshard 127.0.0.1:7000
# 希望迁移多少个槽:4096
# 希望那个id是接收的:7006的id fb83d0f5c350ccf4c2e067f1b2081e236a266b15
# 传入source id :all
# yes
# 集群缩容 7006管理的槽,移走,关闭节点,节点下线,先下从库,再下主
# 第一步:下线迁槽(把7006的1366个槽迁移到7000上)
redis-cli --cluster reshard --cluster-from fb83d0f5c350ccf4c2e067f1b2081e236a266b15 --cluster-to 39ebb8200eea5af3edf893b7ed7b1f0272b6ee8d --cluster-slots 1365 127.0.0.1:7000
yes
redis-cli --cluster reshard --cluster-from fb83d0f5c350ccf4c2e067f1b2081e236a266b15 --cluster-to d61ee6005cd0e93aa8b69a074f89216a5555477e --cluster-slots 1366 127.0.0.1:7001
yes
redis-cli --cluster reshard --cluster-from fb83d0f5c350ccf4c2e067f1b2081e236a266b15 --cluster-to 541aefc31b0b08062e49dc34ee4e86506800addd --cluster-slots 1366 127.0.0.1:7002
yes
#第二步:下线节点 忘记节点,关闭节点
redis-cli --cluster del-node 127.0.0.1:7000 4667744ea620f5cd12448140cbc4eaffe8f8c0c6 # 先下从,再下主,因为先下主会触发故障转移
redis-cli --cluster del-node 127.0.0.1:7000 fb83d0f5c350ccf4c2e067f1b2081e236a266b15
# 第三步:关掉其中一个主,另一个从立马变成主顶上, 重启停止的主,发现变成了从
python操作集群
# rediscluster
# pip3 install redis-py-cluster
from rediscluster import RedisCluster
startup_nodes = [{"host":"127.0.0.1", "port": "7000"},{"host":"127.0.0.1", "port": "7001"},{"host":"127.0.0.1", "port": "7002"}]
# rc = RedisCluster(startup_nodes=startup_nodes,decode_responses=True)
rc = RedisCluster(startup_nodes=startup_nodes)
rc.set("foo", "bar")
print(rc.get("foo"))
缓存优化
缓存更新策略
# 缓存更新策略,redis是缓存数据库,数据存在内存中---》占内存越来越大,内存不够用了,更新策略,剔除一部分数据
LRU/LFU/FIFO算法剔除:例如maxmemory-policy(到了最大内存,对应的应对策略)
LRU -Least Recently Used,没有被使用时间最长的
LFU -Least Frequenty User,一定时间段内使用次数最少的
FIFO -First In First Out,先进先出
LIRS (Low Inter-reference Recency Set)是一个页替换算法,相比于LRU(Least Recently Used)和很多其他的替换算法,LIRS具有较高的性能。这是通过使用两次访问同一页之间的距离(本距离指中间被访问了多少非重复块)作为一种尺度去动态地将访问页排序,从而去做一个替换的选择
缓存穿透,击穿,雪崩
# 缓存穿透
# 描述:
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
# 解决方案:
1 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
3 通过布隆过滤器实现:通过布隆过滤器避免缓存穿透
-在mysql中有的记录,id号放到布隆过滤器中,在查询数据库之前,先判断一下布隆过滤器中有没有该id号,如果没有,说明数据库中也没有,不进行数据库查询,直接返回错误,如果布隆过滤器中有,再进行数据库查询
4 使用频率限制:ip
# 缓存击穿
# 描述:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
# 解决方案:
设置热点数据永远不过期。
# 缓存雪崩
# 描述:
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
# 解决方案:
1 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
3 设置热点数据永远不过期。
分布式id
# 订单号,如何生成的?
# 项目集群化部署,不同机器上生成永不重复的id号
# 在分布式系统之,能够生成全局唯一ID的,它就称之为分布式id (消息id,订单id号。。。。)
# 分布式的id应该具备的特点(了解)
全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则
# 具备以下几点要求
1 平均延迟和TP999延迟都要尽可能低(TP90就是满足百分之九十的网络请求所需要的最低耗时。TP99就是满足百分之九十九的网络请求所需要的最低耗时。同理TP999就是满足千分之九百九十九的网络请求所需要的最低耗时);
2 可用性5个9(99.999%)
3 高QPS:高的并发量
# 如何生成
-1 UUID:简单,没有递增趋势,存在重复的概率[订单号就是使用uuid]
-uuid重复的问题?没有遇到过,分布式id的生成方案,雪花算法
-2 数据库自增:简单,性能低,有递增趋势
-3 Redis生成ID:单线程架构,incr 自增数据, 时间戳+自增 性能,趋势自增,性能高
-4 snowflake(雪花算法)方案 [美团(Leaf)]
这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit
-QPS约为409.6w/s
-优点:
毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
可以根据自身业务特性分配bit位,非常灵活。
-缺点:
强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态
# python 如何实现雪花算法
-pysnowflake 第三方模块
-https://www.cnblogs.com/Blogwj123/p/16614304.html
补充:qps,tps
https://www.cnblogs.com/liuqingzheng/p/16207660.html
# 你有什么想问我们的,了解我们的
-咱们公司项目的业务方向是什么? 医疗行业,物流行业,新能源。。。
-咱们公司项目用户量多少,日活,接口最大qps是多大?
-麦当劳小程序,某个接口并发量比较高:1.6w并发量
-一般1,2百左右,几十
-接口qps:10---》1s钟10个响应回来---》10*60*60*24
-用户量:百万级别用户量,日活几千,破万,qps几百
-淘宝双11,支付宝qps是多少? 9.35万笔/秒
# 1 qps :每秒查询率
# 2 tps:Transactions Per Second,是每秒处理的事务数,包括一条消息入和一条消息出,加上一次用户数据库访问
-一个事务是指一个客户端向服务器发送请求然后服务器做出反应的过程。客户端在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数
-TPS 的过程包括:客户端请求服务端、服务端内部处理、服务端返回客户端。
-例如,访问一个 Index 页面会请求服务器 3 次,包括一次 html,一次 css,一次 js,那么访问这一个页面就会产生一个T,产生三个Q
# 3 并发量
-系统同时处理的请求或事务数,可以直接理解为:系统同时处理的请求数量
QPS = 并发量 / 平均响应时间
并发量 = QPS * 平均响应时间
-例如当前系统QPS为1w,每个请求的响应时间都是2s,那么并发量就是2w
# 4 吐吞量
-吞吐量是指系统在单位时间内处理请求的数量,TPS、QPS都是吞吐量的常用量化指标。
系统吞吐量要素
一个系统的吞吐量(承压能力)与request(请求)对cpu的消耗,外部接口,IO等等紧密关联。
单个request 对cpu消耗越高,外部系统接口,IO影响速度越慢,系统吞吐能力越低,反之越高。
# 5 PV
PV(Page View):页面访问量,即页面浏览量或点击量,用户每次刷新即被计算一次。可以统计服务一天的访问日志得到。
# 6 UV
UV(Unique Visitor):独立访客,统计1天内访问某站点的用户数。可以统计服务一天的访问日志并根据用户的唯一标识去重得到。
# 7 DAU(日活)
-DAU(Daily Active User),日活跃用户数量。常用于反映网站、app、网游的运营情况。
-DAU通常统计一日(统计日)之内,登录或使用了某个产品的用户数(去除重复登录的用户),与UV概念相似
# 8 MAU(月活)
MAU(Month Active User):月活跃用户数量,指网站、app等去重后的月活跃用户数量
# 9 系统服务端性能影响的因素
-衡量服务性能的指标,主要有两个:
QPS(Query Per Second,每秒请求数)
响应时间(Response Time,RT),它可以理解为服务器处理响应的耗时。
-正常情况下,响应时间越短,QPS则越高。
-在单线程的情况下,是呈线性关系。但也不是无限增长,RT总会有极限值。
-多线程时,总QPS = (1000ms/ 响应时间)* 线程数
分布式锁
# https://github.com/SPSCommerce/redlock-py
# 在分布式的系统中,多个系统中的多条线程操作同一个变量,会出现错乱,需要加锁,锁要跨机器,不能使用普通互斥锁,这个锁称之为分布式锁
# 分布式锁要具备的条件
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
# 实现分布式锁的方案
-基于数据库实现分布式锁; (for_update的悲观锁,性能低,一般不用它)
-基于缓存(Redis等)实现分布式锁(主流使用的,性能高)
# python 实现redis分布式锁
https://github.com/SPSCommerce/redlock-py
-安装:pip3 install redlock-py
-安装二: 使用源码包安装
- 下载源码:源码中有 setup.py
python3 setup.py install
-安装方式三:使用whl文件安装
pip3 install xx.whl
# 代码
from redlock import Redlock
import time
dlm = Redlock([{"host": "localhost", "port": 6379, "db": 0}, ])
# 获得锁
my_lock = dlm.lock("my_resource_name",1000)
# 业务逻辑代码
print('sdfasdf')
time.sleep(20)
# 释放锁
dlm.unlock(my_lock)
# 这个代码可以放在任意的节点上,使用的是分布式锁,某个节点获取到锁后,别的节点获取不到,操作数据,释放锁后,别的节点的线程才能操作数据
redis 实现分布式锁(自己实现)
# 实现思想:(setnx)
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
# 代码
import redis
import uuid
import time
from threading import Thread,get_ident
# 连接redis
redis_client = redis.Redis(host="localhost",
port=6379,
# password=password,
db=10)
# 获取一个锁
# lock_name:锁定名称
# acquire_time: 客户端等待获取锁的时间
# time_out: 锁的超时时间
def acquire_lock(lock_name, acquire_time=10, time_out=10):
"""获取一个分布式锁"""
identifier = str(uuid.uuid4())
end = time.time() + acquire_time
lock = "string:lock:" + lock_name
while time.time() < end:
if redis_client.setnx(lock, identifier):
# 给锁设置超时时间, 防止进程崩溃导致其他进程无法获取锁
redis_client.expire(lock, time_out)
return identifier
elif not redis_client.ttl(lock):
redis_client.expire(lock, time_out)
time.sleep(0.001)
return False
# 释放一个锁
def release_lock(lock_name, identifier):
"""通用的锁释放函数"""
lock = "string:lock:" + lock_name
pip = redis_client.pipeline(True)
while True:
try:
pip.watch(lock)
lock_value = redis_client.get(lock)
if not lock_value:
return True
if lock_value.decode() == identifier:
pip.multi()
pip.delete(lock)
pip.execute()
return True
pip.unwatch()
break
except redis.excetions.WacthcError:
pass
return False
def seckill():
identifier = acquire_lock('resource')
print(get_ident(), "获得了锁")
release_lock('resource', identifier)
if __name__ == '__main__':
for i in range(50):
t = Thread(target=seckill)
t.start()
celery
celery介绍
1.celery是一个分布式的异步任务框架
1.1可以完成异步任务:可以提高项目的并发量,之前开启线程做,现在使用celery做
1.2可以完成延迟任务
1.3可以完成定时任务
2.框架
2.1消息中间件(borker)
-提交的任务(函数)都放在这里,celery本身不提供消息中间件,需要借助第三方(redis、rabbitmq)
2.2任务执行单元(worker)
-真正执行任务的地方,一个个进程,执行函数
2.3结果存储(backend)
-函数return的结果存储在这里,celery本身不提供结果存储,借助于第三方(redis、数据库、rabbitmq)
celery快速使用
1.celery是独立的访问
-官网
http://www.celeryproject.org/
'''
1)可以不依赖任何服务器,通过自身命令,启动服务
2)celery服务为为其他项目服务提供异步解决任务需求的
注:会有两个服务同时运行,一个是项目服务,一个是celery服务,项目服务将需要异步处理的任务交给celery服务,celery就会在需要时异步完成项目的需求
人是一个独立运行的服务(django) | 医院也是一个独立运行的服务(celery)
正常情况下,人可以完成所有健康情况的动作,不需要医院的参与;但当人生病时,就会被医院接收,解决人生病问题
人生病的处理方案交给医院来解决,所有人不生病时,医院独立运行,人生病时,医院就来解决人生病的需求
'''
2.安装
pip3 install celery
3.使用
3.1写一个main.py,实例化得到app对象,写函数,任务,注册成celery的任务
from celery import Celery
backend = 'redis://127.0.0.1:6379/1'
broker = 'redis://127.0.0.1:6379/0'
app = Celery('test', backend=backend, broker=broker)
@app.task
def add(a, b):
time.sleep(2)
print(a)
return a + b
3.2在其他文件中提交任务到broker中
from main import add
print('1')
# 执行同步任务
# res = add(3, 4)
# print(res)
# 1 3 7
# 执行异步任务
res = add.delay(3, 4)
print(res)
3.3启动worker,从broker中取任务执行,执行完放到backend中
'''
win:
celery worker -A main -l info -P eventlet # 4.x及之前用这个
celery -A main worker -l info -P eventlet # 5.x及之后用这个
lin,mac:
celery worker -A main -l info
celery -A main worker -l info
'''
3.4再backend中查看任务执行的结果(可以通过代码查看)
from main import app
from celery.result import AsyncResult
id = '7bef14a0-f83e-4901-b4d8-e47aaac09b39'
if __name__ == '__main__':
res = AsyncResult(id=id, app=app)
if res.successful():
result = res.get() # 7
print(result)
elif res.failed():
print('任务失败')
elif res.status == 'PENDING':
print('任务等待中被执行')
elif res.status == 'RETRY':
print('任务异常后正在重试')
elif res.status == 'STARTED':
print('任务已经开始被执行')
celery包结构
1.写一个celery的包,再任意项目中,把包copy进去,导入使用即可
celery_task
__init__.py
celery.py
user_task.py
home_task.py
add_task.py
get_result.py
2.使用
-新建包 celery_task
-在包下新建一个 celery.py
-在里面写app的初始化
-在包里新建user_task.py编写用户相关任务
-在包里新建home_task.py编写首页相关任务
-其它程序,提交任务
-启动worker ---》它可以先启动,在提交任务之前-->包所在的目录下
celery -A celery_task worker -l info -P eventlet
-查看任务执行的结果了
celery_task/celery.py
from celery import Celery
backend = 'redis://127.0.0.1:6379/1'
broker = 'redis://127.0.0.1:6379/0'
app = Celery('test', backend=backend, broker=broker, include=['celery_task.home_task', 'celery_task.user_task'])
celery_task/home_task.py
import time
from .celery import app
@app.task
def add(a, b):
time.sleep(2)
print(f'计算结果为{a + b}')
return a + b
celery_task/user_task.py
import time
from .celery import app
@app.task
def send_sms(mobile, code):
time.sleep(1)
print(f'短信发送成功{mobile},验证码{code}')
return True
add_task.py
from celery_task.user_task import send_sms
# 提交了一个发送短信异步任务
res = send_sms.delay('15976725488', '6666')
print(res) # 13c2208a-07fd-4f27-9691-ea96836a6f66
# 任务执行,要启动worker
get_result.py
from celery_task.celery import app
from celery.result import AsyncResult
id = '13c2208a-07fd-4f27-9691-ea96836a6f66'
if __name__ == '__main__':
res = AsyncResult(id=id, app=app)
if res.successful():
result = res.get() # 7
print(result)
elif res.failed():
print('任务失败')
elif res.status == 'PENDING':
print('任务等待中被执行')
elif res.status == 'RETRY':
print('任务异常后正在重试')
elif res.status == 'STARTED':
print('任务已经开始被执行')
celery异步任务,延迟任务,定时任务
1.异步任务
res = add.delay(3, 4)
print(res)
2.提交延时任务
from celery_task.home_task import add
from datetime import datetime, timedelta
# 得到10秒后的utc时间
eta = datetime.utcnow() + timedelta(seconds=10)
# args内是传给函数的参数,eta是时间对象
res = add.apply_async(args=(200, 50), eta=eta)
print(res)
3.定时任务
3.1启动worker(执行任务)
celery -A celery_task worker -l info -P eventlet
3.2启动beat(提交任务)
celery -A celery_task beat -l info
3.3在app的配置文件中配置
from celery.schedules import crontab
app.conf.beat_schedule = {
'send_sms_task': {
'task': 'celery_task.user_task.send_sms',
'schedule': timedelta(seconds=5), # 每5秒提交
# 'schedule': crontab(hour=8, day_of_week=1), # 每周一早八点
'args': ('1897334444', '7777'),
},
'add_task': {
'task': 'celery_task.home_task.add',
'schedule': crontab(hour=12, minute=10, day_of_week=3), # 每周三12点10分提交
'args': (10, 20),
}
}
django中使用celery
1.把写好的包复制到项目路径下
2.在包内celery.py中加入代码
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')
import django
django.setup()
3.在django的视图类中,导入,提交任务
from .celery import app
from libs.send_tx_sms import send_sms_by_phone
from apps.user.views import UserInfo
@app.task
def send_sms(mobile, code):
send_sms_by_phone(mobile, code)
user = UserInfo.objects.all().filter(mobile=mobile).filter()
print(f'给{user.username}发送短信发送成功:{mobile},验证码{code}')
return True
4.启动worker,beat
秒杀逻辑
1.秒杀事件
向后端秒杀接口发送请求,发送完立马起了一个定时任务,每个5s,向后端查看一下是否秒杀成功,如果秒杀没成功,定时任务继续执行,如果秒杀成功了,清空定时任务,弹窗告诉他
2.前端
<template>
<div>
<el-button type="danger" plain @click="handleClick">一键秒杀</el-button>
</div>
</template>
<script>
export default {
name: "Seckill",
methods: {
handleClick() {
this.$axios.get(this.$settings.BASE_URL + 'userinfo/seckill/').then(res => {
if (res.data.code == 100) {
let task_id = res.data.id
this.$message({
message: res.data.msg,
type: 'error'
});
// 起个定时事件,每隔5秒向后端查询一下是否秒杀成功
let t = setInterval(() => {
this.$axios.get(this.$settings.BASE_URL + 'userinfo/get_result/?id=' + task_id).then(res => {
// 秒杀结束了,要么成功,要么失败了
if (res.data.code == 100 || res.data.code == 101) {
alert(res.data.msg)
// 销毁掉定时任务
clearInterval(t)
} else if (res.data.code == 102) {
alert(res.data.msg)
}
}
)
}, 5000)
}
})
}
}
}
</script>
3.后端
3.1秒杀接口
from celery_task.user_task import seckill_task
from django.http import JsonResponse
def seckill(request):
res = seckill_task.delay()
return JsonResponse({'code': 100, 'msg': '正在排队', 'id': str(res)})
3.2查询是否秒杀成功接口
from celery_task.celery import app
from celery.result import AsyncResult
def get_result(request):
task_id = request.GET.get('id')
res = AsyncResult(id=task_id, app=app)
if res.successful():
result = res.get()
return JsonResponse({'code': 100, 'msg': str(result)})
elif res.failed():
return JsonResponse({'code': 101, 'msg': '秒杀失败'})
elif res.status == 'PENDING':
return JsonResponse({'code': 100, 'msg': '还在排队'})
3.3秒杀任务
@app.task
def seckill_task():
time.sleep(3) # 秒杀需要3秒
res = random.choice([True, False]) # 可能成功,可能不成功
if res:
return '秒杀成功'
return '很遗憾,你没有秒到'
双写一致性
接口加缓存
首页轮播图接口,加缓存,提交了接口的响应速度、并发量
from utils.response import APIResponse
from django.core.cache import cache
class BannerView(GenericViewSet, CommonListModelMixin):
queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[:BANNER_COUNT]
serializer_class = BannerSerializer
def list(self, request, *args, **kwargs):
result = cache.get('banner_list')
if result:
print('走缓存,速度快')
return APIResponse(result=result)
else:
print('走数据库,速度慢')
res = super().list(request, *args, **kwargs)
result = res.data.get('result')
cache.set('banner_list', result)
return res
celery定时任务实现双写一致性
1.在数据加到缓存中后,如果mysql数据变了,由于请求的都是缓存的数据,导致mysql和redis的数据不一致
2.双写一致性问题
1 修改mysql数据库,删除缓存 (缓存的修改是在后)
2 修改数据库,修改缓存 (缓存的修改是在后)
3 定时更新缓存 (针对于实时性不是很高的接口适合定时更新)
'给首页轮播图接口加入了缓存,出现了双写一致性问题,使用定时更新来解决双写一致性的问题【会存在不一致的情况,我们可以忽略】---》定时任务,celery的定时任务'
- home_task.py
from celery import Celery
from datetime import timedelta
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')
import django
django.setup()
backend = 'redis://127.0.0.1:6379/1'
broker = 'redis://127.0.0.1:6379/0'
app = Celery('test', backend=backend, broker=broker, include=['celery_task.home_task'])
from celery.schedules import crontab
app.conf.beat_schedule = {
'update_banner': {
'task': 'celery_task.home_task.update_banner',
'schedule': timedelta(seconds=20), # 20秒更新轮播图
'args': (),
}
}
- celery.py
from .celery import app
from home.models import Banner
from django.conf import settings
from settings.common_settings import *
from django.core.cache import cache
from home.serializer import BannerSerializer
@app.task
def update_banner():
queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[
:BANNER_COUNT]
ser = BannerSerializer(instance=queryset, many=True)
for item in ser.data:
item['image'] = settings.HOST_URL + item['image']
cache.set('banner_list', ser.data)
return True