浅学Redis
一、Redis介绍
1、什么是redis
Redis是一个key-value存储系统,和Memcached类似,它支持存储的value类型相对更多,包括:string(字符串)、list(列表)、set(集合)、zset(sorted set--》有序集合)、hash(哈希类型)。这些数据类型都支持push、pop、add、remove及取交集、并集和差集等更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘,或者班修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis是一个高性能的key-value数据库。Redis的出现,很大程度补偿了memcached这类key-value存储的不足,在部分场合可以对关系数据库起到很好的补充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,使用很方便。
Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。
2、总结
redis介绍总结:
- 1、redis是缓存数据库
- 2、redis是非关系型数据库。k-v键值对存储数据,没有表的概念
- 3、redis是C语言编写的服务,速度非常快
redis为什么速度那么快:
- 1、纯内存操作所以速度如此之快
- 2、网络模型使用的IO多路复用所以高并发
- 3、6.X版本之前都是单进程、单线程架构,没有线程进程件切换因此资源消耗更少
二、Redis安装与启动
1、Redis安装步骤
-
windows下安装
mac与Linux系统直接在redis官网源码下载编译安装即可,但是Windows不支持该方法,所以Windows用户可以去这个地址下载:https://github.com/tporadowski/redis/releases(因为GitHub是国外的网站访问可能会很慢,耐心等待一下)
中文网:http://redis.cn/
-
安装
2、Redis启动服务
-
方式一:
在计算机服务里手动启动redis服务
右键点击此电脑→→管理→→服务与应用程序→→服务→→找到Redis双击打开→→启动
-
方式二:
在cmd终端下输入一下两个命令即可启动
redis-server # 启动服务端
redis-cli # 启动服务端
3、图形化界面使用Redis
-
Redis的图形化客户端有很多,这里推荐使用Redis Desktop Manager
-Redis Desktop Manager :开源的,原来免费,后来收费了 推荐用(mac,win,linux 都有) -Qt5 qt是个平台,专门用来做图形化界面的 -可以使用c++写 -可以使用python写 pyqt5 使用python写图形化界面 (少量公司再用) -安装包:resp-2022.1.0.0.exe 一路下一步,安装完启动起来
-
安装resp
下载安装包后,一路下一步安装即可
安装成功后打开软件,会出现连接配置窗口,我们点击左边的新建连接后,会出现下图窗口
接着我们输入配置信息,测试连接成功后,点击确认即可
通过图形化界面,我们可以看到redis默认有16个库,默认连进去就是第0个
ps:我们在连接的时候也可以使用redis协议连接
三、Redis普通连接和连接池
1、简单使用方式
在cmd中,redis的简单使用方式如下
127.0.0.1:6379> set name zsm
OK
127.0.0.1:6379> get name
"zsm"
2、pycharm使用redis准备工作
使用pycharm操作redis的时候,pycharm就相当于一个客户端
打开pycharm后,打开terminal界面,输入命令安装redis模块
pip install redis
-
普通连接
# 1、导入模块的Redis类 from redis import Redis # 2、实例化得到对象 conn = Redis(host='127.0.0.1', port=6379) # 3、使用conn,操作redis ## 设置值 conn.set('name','jason') ## 取值 res = conn.get('name') # 返回数据时bytes格式 conn.close()
-
连接池连接
前面我们提到redis服务支持高并发,而redis是基于socket建立连接的,在客户端主动断开连接之前,会一直保持连接的状态
因此当访问量很大的时候,上面的代码就会出现连接数量太大,线程太多,导致性能降低的情况
因此我们这里用连接池来限制最大的连接数量
ps:这里可以跟mysql对比记忆,mysql没有使用连接池,一个请求就会创建一个连接,并发数过高也会导致性能降低(但是我们可以自行创建,django-db-connection-pool模块)
简单实现线程池
这里只是编写了线程池
import redis POOL = redis.ConnectionPool(max_connections=10, host='127.0.0.1', port=6379) con = redis.Redis(connection_pool=POOL) print(con.get('name')) con.close()
多线程测试连接池
这里我们利用多线程测试连接池的工作情况
在测试的时候我们会发现,当连接池全被占用后,其他线程会因为没有连接可用,而报错,但是并不影响后续的线程运行
我们把连接池的对象POOL当成配置文件存到pool.py中,因为连接池需要做成单例模式,否侧就会在创建线程的时候,创建新的连接池,更加降低性能
pool.py
import redis POOL = redis.ConnectionPool(max_connections=10, host='127.0.0.1', port=6379) # 创建一个大小为10的redis连接池
测试代码
from threading import Thread import redis from pool import POOL ''' 要把POOL做成单例,否则有可能,在每个线程中创建了一个池,从池中拿一个连接,性能更低 多种方式,使用模块导入的方式 多种方式,使用模块导入的方式 ''' def task(): con = redis.Redis(connection_pool=POOL) print(con.get('name')) for i in range(100): t = Thread(target=task) t.start()
四、Redis五大数据类型
5种数据类型,value类型
-字符串:用的最多,做缓存;做计数器
-列表: 简单的消息队列
-字典(一些语言叫他hash):缓存
-集合:去重
-有序集合:排行榜
redis={
k1:'123', 字符串
k2:[1,2,3,4], 列表/数组
k3:{1,2,3,4} 集合
k4:{name:lqz,age:12} 字典/哈希表
k5:{('lqz',18),('egon',33)} 有序集合
}
1、String(字符串类型)
String操作,redis中的String在在内存中按照一个name对应一个value来存储。如图:
-
set(name, value, ex=None, px=None, nx=False, xx=False)
在Redis中设置值,默认,不存在创建,存在则修改 参数: ex:过期时间(秒) px:过期时间(毫秒) nx:如果设置为True,则只有name不存在时,当前set操作才执行,值存在,就修改不了,执行没结果 xx:如果设置为True,则只有name存在时,当前set操作才执行,值存在才能修改,值不存在,不会设置新值
-
setnx(name, value)
设置值,只有name不存在时,执行设置操作(添加),如果存在,不会修改 相当于: set('name','jason',nx=True)
-
setex(name, value, time)
设置值 参数: time,过期时间(数字秒 或 timedelta对象) setex('name', 3, 'jason') 等同于: set('name', 'jason', ex=3)
-
psetex(name, time_ms, value)
设置值 参数: time_ms,过期时间(数字毫秒 或 timedelta对象) psetex('name', 3000, 'jason') 等同于: set('name', 'jason', px=3)
-
mset(*args, **kwargs)
批量设置值 如: mset(k1='v1', k2='v2')
-
get(name)
获取值
-
mget(keys, *args)
批量获取 如: mget('k1', 'k2') 或 r.mget(['k3', 'k4'])
-
getset(name, value)
设置新值并获取原来的值 getset('wife','迪丽热巴')
-
getrange(key, start, end)
获取子序列(根据字节获取,非字符) 参数: name,Redis 的 name start,起始位置(字节) end,结束位置(字节) 不是字符长度 前闭后闭区间 getrange('wife', 0, 2) 如: "迪丽热巴" ,0-3表示 "迪"
-
setrange(name, offset, value)
修改字符串内容,从指定字符串索引开始向后替换(新值太长时,则向后添加) 参数: offset,字符串的索引,字节(一个汉字三个字节) value,要设置的值 setrange('wife',2,'bbb')
-
setbit(name, offset, value)
对name对应值的二进制表示的位进行操作 参数: name,redis的name offset,位的索引(将值变换成二进制后再进行索引) value,值只能是 1 或 0 注:如果在Redis中有一个对应: n1 = "foo", 那么字符串foo的二进制表示为:01100110 01101111 01101111 所以,如果执行 setbit('n1', 7, 1),则就会将第7位设置为1, 那么最终二进制则变成 01100111 01101111 01101111,即:"goo"
-
getbit(name, offset)
获取name对应的值的二进制表示中的某位的值 (0或1)
-
bitcount(key, start=None, end=None)
获取name对应的值的二进制表示中 1 的个数 参数: key,Redis的name start,位起始位置 end,位结束位置
-
bitop(operation, dest, *keys)
获取多个值,并将值做位运算,将最后的结果保存至新的name对应的值 参数: operation,AND(并) 、 OR(或) 、 NOT(非) 、 XOR(异或) dest, 新的Redis的name *keys,要查找的Redis的name 如: bitop("AND", 'new_name', 'n1', 'n2', 'n3') 获取Redis中n1,n2,n3对应的值,然后讲所有的值做位运算(求并集),然后将结果保存 new_name 对应的值中
-
strlen(name)
返回name对应值的字节长度(一个汉字3个字节)
-
incr(self, name, amount=1)
自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。 参数: name,Redis的name amount,自增数(必须是整数) 注:同incrby
-
incrbyfloat(self, name, amount=1.0)
自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。 参数: name,Redis的name amount,自增数(浮点型)
-
decr(self, name, amount=1)
自减 name对应的值,当name不存在时,则创建name=amount,否则,则自减。 参数: name,Redis的name amount,自减数(整数)
-
append(key, value)
在redis name对应的值后面追加内容 参数: key, redis的name value, 要追加的字符串
-
操作代码
import redis conn = redis.Redis() # 1 set(name, value, ex=None, px=None, nx=False, xx=False) # ex,过期时间(秒) # px,过期时间(毫秒) # nx,如果设置为True,则只有name不存在时,当前set操作才执行, 值存在,就修改不了,执行没效果 # xx,如果设置为True,则只有name存在时,当前set操作才执行,值存在才能修改,值不存在,不会设置新值 # conn.set('hobby','篮球',ex=3) # conn.set('hobby','篮球',px=3) # conn.set('name','lqz',nx=True) # conn.set('name','lqz',nx=False) # conn.set('hobby','篮球',xx=True) # conn.set('hobby','篮球',xx=False) # redis---》实现分布式锁,底层基于nx实现的 # 2 setnx(name, value) # 等同于:conn.set('name','lqz',nx=True) # conn.setnx('name', '刘亦菲') # 3 setex(name, value, time) # 等同于:conn.set('name','lqz',ex=3) # conn.setex('wife', 3, '刘亦菲') # 4 psetex(name, time_ms, value) # conn.psetex('wife',3000,'刘亦菲') # 5 mset(*args, **kwargs) # conn.mset({'wife': '刘亦菲', 'hobby': '篮球'}) # 6 get(name) # print(str(conn.get('wife'),encoding='utf-8')) # print(conn.get('wife')) # 7 mget(keys, *args) # res=conn.mget('wife','hobby') # res=conn.mget(['wife','hobby']) # print(res) # 8 getset(name, value) # res=str(conn.getset('wife','迪丽热巴'),encoding='utf-8') # res=conn.getset('wife','迪丽热巴') # print(res) # 9 getrange(key, start, end) # res = str(conn.getrange('wife', 0, 2), encoding='utf-8') # 字节长度,不是字符长度 前闭后闭区间 # print(res) # 10 setrange(name, offset, value) # conn.setrange('wife',2,'bbb') # ---- 比特位---操作 # 11 setbit(name, offset, value) # 12 getbit(name, offset) # 13 bitcount(key, start=None, end=None) # ---- 比特位---操作 # 14 bitop(operation, dest, *keys) 获取多个值,并将值做位运算,将最后的结果保存至新的name对应的值 # 15 strlen(name) # res=conn.strlen('hobby') # 统计字节长度 # print(res) # 16 incr(self, name, amount=1) # 自增,不会出并发安全问题,单线程架构,并发量高 # conn.incr('age') # # incrby # 17 incrbyfloat(self, name, amount=1.0) # conn.incrbyfloat('age',1.2) # 18 decr(self, name, amount=1) # conn.decrby('age') # conn.decrby('age',-1) # 19 append(key, value) # conn.append('hobby','sb') print(conn.strlen('hobby')) conn.close()
我们需要记住的字符串类型的操作
set 添加值
get 获取值
strlen 字节长度
incr 自增1
2、List(列表类型)
List操作,redis中的List在在内存中按照一个name对应一个List来存储。如图:
-
lpush(name,values)
# 在name对应的list中添加元素,每个新的元素都添加到列表的最左边 # 如: # r.lpush('oo', 11,22,33) # 保存顺序为: 33,22,11 # 扩展: # rpush(name, values) 表示从右向左操作
-
lpushx(name,value)
# 在name对应的list中添加元素,只有name已经存在时,值添加到列表的最左边 # 更多: # rpushx(name, value) 表示从右向左操作
-
llen(name)
# name对应的list元素的个数
-
linsert(name, where, refvalue, value))
# 在name对应的列表的某一个值前或后插入一个新值 # 参数: # name,redis的name # where,BEFORE或AFTER(小写也可以) # refvalue,标杆值,即:在它前后插入数据(如果存在多个标杆值,以找到的第一个为准) # value,要插入的数据
-
r.lset(name, index, value)
# 对name对应的list中的某一个索引位置重新赋值 # 参数: # name,redis的name # index,list的索引位置 # value,要设置的值
-
r.lrem(name, value, num)
# 在name对应的list中删除指定的值 # 参数: # name,redis的name # value,要删除的值 # num, num=0,删除列表中所有的指定值; # num=2,从前到后,删除2个; # num=-2,从后向前,删除2个
-
lpop(name)
# 在name对应的列表的左侧获取第一个元素并在列表中移除,返回值则是第一个元素 # 更多: # rpop(name) 表示从右向左操作
-
lindex(name, index)
在name对应的列表中根据索引获取列表元素
-
lrange(name, start, end)
# 在name对应的列表分片获取数据 # 参数: # name,redis的name # start,索引的起始位置 # end,索引结束位置 print(re.lrange('aa',0,re.llen('aa')))
-
ltrim(name, start, end)
# 在name对应的列表中移除没有在start-end索引之间的值 # 参数: # name,redis的name # start,索引的起始位置 # end,索引结束位置(大于列表长度,则代表不移除任何)
-
rpoplpush(src, dst)
# 从一个列表取出最右边的元素,同时将其添加至另一个列表的最左边 # 参数: # src,要取数据的列表的name # dst,要添加数据的列表的name
-
blpop(keys, timeout)
# 将多个列表排列,按照从左到右去pop对应列表的元素 # 参数: # keys,redis的name的集合 # timeout,超时时间,当元素所有列表的元素获取完之后,阻塞等待列表内有数据的时间(秒), 0 表示永远阻塞 # 更多: # r.brpop(keys, timeout),从右向左获取数据爬虫实现简单分布式:多个url放到列表里,往里不停放URL,程序循环取值,但是只能一台机器运行取值,可以把url放到redis中,多台机器从redis中取值,爬取数据,实现简单分布式
-
brpoplpush(src, dst, timeout=0)
# 从一个列表的右侧移除一个元素并将其添加到另一个列表的左侧 # 参数: # src,取出并要移除元素的列表对应的name # dst,要插入元素的列表对应的name # timeout,当src对应的列表中没有数据时,阻塞等待其有数据的超时时间(秒),0 表示永远阻塞
-
操作代码
import redis conn = redis.Redis() # 1 lpush(name, values) 从错侧插入 # conn.lpush('girls', '刘亦菲', '迪丽热巴') # conn.lpush('girls', '周淑怡') # 2 rpush(name, values) 表示从右向左操作 # conn.rpush('girls', '小红') # 3 lpushx(name, value) # conn.lpushx('boys','小刚') # conn.lpush('boys','小刚') # conn.lpushx('girls','小刚') # 4 rpushx(name, value) 表示从右向左操作 # 5 llen(name) # res = conn.llen('girls') # print(res) # 6 linsert(name, where, refvalue, value)) # conn.linsert('girls','before','迪丽热巴','古力娜扎') # conn.linsert('girls', 'after', '小红', '小绿') # conn.linsert('girls', 'after', '小黑', '小嘿嘿') # 没有标杆,插入不进去 # 7 r.lset(name, index, value) # 按位置改值 # conn.lset('girls',1,'xxx') # 8 r.lrem(name, value, num) # conn.lrem('girls',1,'xxx') # 从左侧开始,删除1个 # conn.lrem('girls',-1,'xxx') # 从右侧开始,删除1个 # conn.lrem('girls',0,'xxx') # 从左开始,全删除 # 9 lpop(name) # res=conn.lpop('girls') # print(res) # 10 rpop(name) 表示从右向左操作 # 11 lindex(name, index) # res = str(conn.lindex('girls', 1), encoding='utf-8') # print(res) # 12 lrange(name, start, end) # res=conn.lrange('girls',0,0) # 前闭后闭区间 # print(res) # 13 ltrim(name, start, end) # conn.ltrim('girls',2,3) # 14 rpoplpush(src, dst) # 15 blpop(keys, timeout) # 记住 ,可以做消息队列使用 阻塞式弹出,如果没有,就阻塞 # res=conn.blpop('boys') # print(res) # 16 r.brpop(keys, timeout),从右向左获取数据 # 17 brpoplpush(src, dst, timeout=0) conn.close()
我们需要记住的列表类型的操作
lpush
lpop
llen
lrange
3、Hash(哈希类型)
Hash操作,redis中Hash在内存中的存储格式如下图:
-
hset(name, key, value)
# name对应的hash中设置一个键值对(不存在,则创建;否则,修改) # 参数: # name,redis的name # key,name对应的hash中的key # value,name对应的hash中的value # 注: # hsetnx(name, key, value),当name对应的hash中不存在当前key时则创建(相当于添加)
-
hmset(name, mapping)
# 在name对应的hash中批量设置键值对 # 参数: # name,redis的name # mapping,字典,如:{'k1':'v1', 'k2': 'v2'} # 如: # r.hmset('xx', {'k1':'v1', 'k2': 'v2'})
-
hget(name,key)
# 在name对应的hash中获取根据key获取value
-
hmget(name, keys, *args)
# 在name对应的hash中获取多个key的值 # 参数: # name,reids对应的name # keys,要获取key集合,如:['k1', 'k2', 'k3'] # *args,要获取的key,如:k1,k2,k3 # 如: # r.mget('xx', ['k1', 'k2']) # 或 # print r.hmget('xx', 'k1', 'k2')
-
hgetall(name)
# 获取name对应hash的所有键值 print(re.hgetall('xxx').get(b'name'))
-
hlen(name)
# 获取name对应的hash中键值对的个数
-
hkeys(name)
# 获取name对应的hash中所有的key的值
-
hvals(name)
# 获取name对应的hash中所有的value的值
-
hexists(name, key)
# 检查name对应的hash是否存在当前传入的key
-
hdel(name,*keys)
# 将name对应的hash中指定key的键值对删除 print(re.hdel('xxx','sex','name'))
-
hincrby(name, key, amount=1)
# 自增name对应的hash中的指定key的值,不存在则创建key=amount # 参数: # name,redis中的name # key, hash对应的key # amount,自增数(整数)
-
hincrbyfloat(name, key, amount=1.0)
# 自增name对应的hash中的指定key的值,不存在则创建key=amount # 参数: # name,redis中的name # key, hash对应的key # amount,自增数(浮点数) # 自增name对应的hash中的指定key的值,不存在则创建key=amount
-
hscan(name, cursor=0, match=None, count=None)
# 增量式迭代获取,对于数据大的数据非常有用,hscan可以实现分片的获取数据,并非一次性将数据全部获取完,从而放置内存被撑爆 # 参数: # name,redis的name # cursor,游标(基于游标分批取获取数据) # match,匹配指定key,默认None 表示所有的key # count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数 # 如: # 第一次:cursor1, data1 = r.hscan('xx', cursor=0, match=None, count=None) # 第二次:cursor2, data1 = r.hscan('xx', cursor=cursor1, match=None, count=None) # ... # 直到返回值cursor的值为0时,表示数据已经通过分片获取完毕
-
hscan_iter(name, match=None, count=None)
# 利用yield封装hscan创建生成器,实现分批去redis中获取数据 # 参数: # match,匹配指定key,默认None 表示所有的key # count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数 # 如: # for item in r.hscan_iter('xx'): # print item
五、Redis通用操作
所有类型都支持的操作,不指定类型
delete(*names)
exists(name)
keys(pattern='*')
expire(name,time)
rename(src, dst)
move(name, db)
randomkey()
type(name)
-
delete(*names)
根据K删除Redis中的任意数据类型
-
exists(name)
检测redis的name是否存在
-
keys(pattern=‘*’)
根据模型获取redis的name 更多: KEYS * 匹配数据库中所有key KEYS h?llo 匹配 hello,hallo和hxllo等 KEYS h*llo 匹配 hllo 和 heeeeello 等。 KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo
-
expire(name,time)
为某个redis的某个name设置超时时间
-
rename(src, dst)
对redis的name重命名为
-
move(name, db)
将redis的某个值移动到指定db下
-
randomkey()
随机获取一个redis的name(不删除)
-
type(name)
获取name对应值的类型
点击查看代码
import redis
conn = redis.Redis()
# 1 delete(*names)
# conn.delete('name', 'userinfo2')
# conn.delete(['name', 'userinfo2']) # 不能用它
# conn.delete(*['name', 'userinfo2']) # 可以用它
# 2 exists(name)
# res=conn.exists('userinfo')
# print(res)
# 3 keys(pattern='*')
# res=conn.keys('w?e') # ?表示一个字符, * 表示多个字符
# print(res)
# 4 expire(name ,time)
# conn.expire('userinfo',3)
# 5 rename(src, dst)
# conn.rename('hobby','hobby111')
# 6 move(name, db))
# conn.move('hobby111',8)
# 7 randomkey()
# res=conn.randomkey()
# print(res)
# 8 type(name)
# print(conn.type('girls'))
print(conn.type('age'))
conn.close()
六、Redis管道
redis-py默认在执行每次请求都会创建(连接池申请连接)和断开(归还连接池)一次连接操作,如果想要在一次请求中指定多个命令,则可以使用pipeline实现一次请求指定多个命令,并且默认情况下一次pipeline是原子性操作
事务的四大特性:
-原子性
-一致性
-隔离性
-持久性
redis支持事务吗
单实例才支持所谓的事务,支持事务是基于管道的
执行命令 一条一条执行
-张三 金额:100
conn.decr('zhangsan_je', 100)
-挂了
-李四 jine:100
conn.incr('lisi_je', 100)
把这两条命令,放到一个管道中,先不执行,执行excute,一次性都执行完成
conn.decr('zhangsan_je', 100) conn.incr('李四_je',100)
如何使用
import redis
conn = redis.Redis()
p=conn.pipeline(transaction=True)
p.multi()
p.decr('zhangsan_je', 100)
# raise Exception('崩了')
p.incr('lisi_je', 100)
p.execute()
conn.close()
七、django中使用redis
-
方式一:自定义包方案(通用的,不针对与框架,所有框架都可以用)
# 第一步:写一个pool.py import redis POOL = redis.ConnectionPool(max_connections=100) # 第二步:以后在使用的地方,直接导入使用即可 coon = redis.Redis(connection_pool=POOL) conn.incr('count') res = conn.get('count')
-
方式二:django方案
-
方案一:django的缓存使用redis【推荐使用】
settings.py中配置
CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": {"max_connections": 100} # "PASSWORD": "123", } } }
在使用redis的地方:
cache.set('count', res+1)
pickle序列化后,存入的
-
方案二:django-redis模块【第三方模块】
from django_redis import get_redis_connection def test_redis(request): conn = get_redis_connection() res = print(conn.get('count')) return JsonResponse({'count': '今天这个接口被访问的次数为:%s' % res}, json_dumps_params={'ensure_ascii':False})
-