Redis 基础
Redis 基础
- Redis 没有建库建表的操作,默认会自动建16个库,切换库操作: select (0-15)
- redis特性
- redis 与其他 key-value 缓存产品有以下三个特点:
- redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用
- redis 不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
- redis支持数据的备份,即master-slave模式的数据备份
- redis 优势
- 性能极高-redis能读的速度是110000次/s,写的速度是81000次/s
- 丰富的数据类型-redis支持二进制案例的Strings,Lists,Hashes,Sets 及 Orderd Sets 数据类型操作
- 原子 - Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行
- 丰富的特性-Redis还支持 publish/subscribe,通知,key过期等特性
- 推荐阅读
Redis 的数据类型
-
string 字符串
- set --- 设置键值,存在即修改不存在即创建
- setex --- 设置具有有效期的键值对,秒为单位
- mset --- 一次设置多个键值对
- append --- 对指定的键后面追加字符 append key new_value
- get --- 获取键 key 的值,不存在就返回 nil
- mget --- 同时获取多个键的值value
- incr --- 计数加1
- decr --- 计数减1
- incrby --- 计数加 n
-
keys 键的操作命令
- keys [pattern] --- keys * 获取所有键 key 的值 value
- exists --- 判断键 key 是否存在,存在为 1 不存在为 0
- type --- 查看键对应 value 的类型
- del --- 删除键及对应的值
- expire --- 设置key的过期时间,秒为单位
- getrange --- 截取字符串 getrange key start end 负数表示从字符串最后开始计数
- ttl --- 查看键的有效时间
-
hash 哈希(相当于一个类中有多个键值对;值的类型为string)
- hset --- 设置某个键中属性和值(一个键中的多个属性和值)
- hmset --- 设置一个键中的多个属性和值
- hkeys --- 获取指定键所有属性
- hget --- 获取某个key的一个属性所对应的值
- hmget --- 获取多个字段
- hvals --- 获取某个hash键的属性所对应的所有值
- hdel --- 删除整个hash键的属性(对应值会同时删掉)
-
list 列表(有序的,相当于数组,列表元素类型string,按照插入顺序排序)
-
lpush --- 在列表左侧插入数据
-
rpush --- 在列表右侧插入数据
-
linsert --- 在指定元素的前(before)或后(after)插入新元素
-
lrange --- 查看,返回列表里指定范围内的元素 lrange key start stop 索引从左侧开始第一个元素下标为0,负数是从尾部 -1开始
-
lset --- 设置指定索引位置的元素值 lset key index value
-
lrem --- 删除指定元素(也可以删除重复元素的其中几个 lrem aa 2 b表示aa列表中从头开始查找b删除两个,负数为从后开始计数)
-
ltrim --- 截取修建,根据范围删除数据 ltrim key start stop [start stop]区间内的元素全部删除
-
lpop --- 从左侧删除元素
-
rpop --- 从右侧删除元素
-
-
set 集合(无序集合,集合顺序和插入顺序无关,string类型,元素具有唯一性,不重复,没有修改操作)
-
sadd --- 添加元素
-
smembers --- 获取键的所有元素
-
srem --- 删除指定键里面的值
-
sismember --- 判断是否包含
-
-
zset 有序集合(sorted set,有序集合,string类型,元素具有唯一性,不重复,每个元素都有一个double类型的score表示权重,没有修改操作)
- zadd --- 向集合中增加权重和对应元素值
- zrange --- 遍历元素(按分数从小到大)
- zrevrange ---反向遍历元素(从大到小)
- zrangebyscore --- 根据权重取值;zrangebyscore key min max 返回 score(权重 值)在 min 和 max之间的成员
- zscore --- 查询集合中某个元素的权重
- zrem --- 删除集合中指定元素
- zremrangebyscore --- 根据集合中的权重值范围删除元素
- zincrby --- 元素的分数计数加n
-
python 编程操作redis
- 下载库 pip3 install redis
通过init创建对象,指定参数host、port与指定的服务器和端⼝连接,host默认为localhost,port默认为6379,db默认为0
示例代码:
from redis import *
if __name__ == '__main__':
try:
# decode_responses='utf8' 对获取到的值进行解码
# 建立连接:host port db 可以不写,默认是本地连接
src = StrictRedis(host='localhost',port=6379, db=0 ,decode_responses='utf8')
# 执行操作
res = src.set('n', '黑马')
print("设置成功与否", res)
n = src.get('n')
print("获取的值", n)
a = src.delete("n")
print("删除个数", a)
except Exception as e:
print(e)
- 解决:命令行查看中文乱码
# 在命令行查看某个键的中文值不乱码的方法:
[luojie@11:47:38]redis-cli --raw
127.0.0.1:6379> get name
黑马
127.0.0.1:6379> keys *
name
Redis 持久化
RDB做镜像全量持久化,AOF做增量持久化。因为RDB会很耗时,不够实用,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令实现完整恢复重启之前的状态
持久化的两种方式
Redis 持久化是其高可用中比较重要的一个环节,因为 Redis数据在内存的特性,持久化是必要的,Redis 的持久化有两种方式:
-
RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化
-
AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所有没有任何磁盘寻址的开销,所有很快,有点像Mysql中的binlog日志
上述两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备,AOF更适合做热备
注:两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整
持久化两种方式的优缺点
RDB:
- 优点:以快照的方式生成多个数据文件,每个数据文件都代表了某一时刻Redis里面的数据,这种方式,更适合做冷备,设置定时任务来执行。RDB对Redis的性能影响非常小,是因为在同步数据的时候它只是fork了一个子进程去做持久化,而且在数据恢复是速度也比AOF来的快
- 缺点:RDB都是快照文件,都是默认五分钟甚至更久的时间才去生成一次,这意味着你这次同步到下一次同步这中间5分钟的时间很可能会丢失。AOF则最多丢一秒的数据,数据完整性高下立判。还有RDB在生成数据快照时,如果文件很大就会很耗时。
AOF:
- 优点:AOF是一秒一次去通过后台的线程fsync操作,最多会丢失1秒的数据。AOF在对日志文件进行操作的时候是以append-only的方式去写的,它只是追加的方式写数据,自然就少了很多磁盘寻址的开销,写入性能惊人,文件也不容易破损。AOF的日志是通过一个非常可读的方式记录的,这样的特性适合做灾难性数据误删除的紧急恢复。
- 缺点:一样的数据,AOF文件比RDB还要大。随着时间的流逝,AOF文件会越来越大。AOF开启后,Redis支持写的QPS会比RDB支持写的要低,它不是每秒都要去异步刷新一次日志fsync
两种备份方式如何选择
两种都要,如果要进行数据恢复时,用RDB进行第一时间的快速恢复,然后用AOF做数据补全,冷备热备一起使用
Redis主从中的数据如何同步
- 当启动一台slave的时候,他会发送一个psync命令个master,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做到的第一件事就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命令都发给slave;数据传输过程中断网了怎么办?传输过程中出现连接断开会自动重连,并且把连接后缺少的数据补上
Redis 集群中,有卡槽的可以直接存取数据,没有卡槽的使用重定向存取数据
Redis的过期删除策略
- 定时删除:每个设置过期时间的key都创建一个定时器,到过期时间会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源进行计时和处理过期数据,从而影响缓存的响应时间和吞吐量
- 定期删除(默认策略):每隔一定时间100s,扫描数据库中一部分设置了有效期的key,并清除其中已过期的key。该策略是另外两种的折中方案。通过调整定时扫描的时间间隔和每次扫面的限定耗时,可以在不同情况下使用CPU和内存资源达到最优的平衡效果
- 惰性删除(默认策略):只有当访问一个key时,才会判断该key是否已过期,过期则清除(返回nil)。该策略可以最大化节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清楚,占用大量内存
- 如果定期没能删除过期的数据key就会用到内存淘汰机制!:所谓内存淘汰机制,是指在Redis允许使用的内存达到上限时,如何淘汰已有数据及处理新的写入需求。Redis提供多种缓存淘汰策略,最常用的LRU和LFU
- LRU(Least recently used 最后使用时间策略):LRU算法根据数据的历史访问记录来进行淘汰数据,优先淘汰最近没有使用过的数据
- 基本思路
- 新数据插入到列表头部
- 每当缓存命中(访问),则将数据移动到列表头部
- 当列表满的时候,将列表尾部的数据丢弃
- 存在问题:单独按照最后使用时间进行数据淘汰,可能会将一些使用频繁的数据删除,可以调整淘汰时间来删除数据
- 基本思路
- LFU(Least Frequently Used 最少使用次数策略)
- redis4.x 后支持LFU策略
- 优先淘汰使用率最低的数据
- 考虑到新添加的数据往往使用次数要低于旧数据,LFU还实现了定期衰减机制
- LFU的缺点:
- 需要每条数据维护一个使用计数
- 还需要定期衰减
- LRU(Least recently used 最后使用时间策略):LRU算法根据数据的历史访问记录来进行淘汰数据,优先淘汰最近没有使用过的数据
Redis的淘汰策略
- allkeys-lfu: 当内存不足以容纳新写入数据时,在键空间中,优先移除使用次数最少的key。
- (常用)volatile-lfu: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,优先移除使用次数最少的key。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,优先移除最近没有使用过的key。
- (常用)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,优先移除最近没有使用过的key。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
思考:如何将2000w条mysql数据,在redis中只存储20w条热点数据?
- 设置淘汰策略volatile-lfu
- 计算20w条数据的大致内存大小,设置成上限
- 2000w条数据都去过redis,到达内存上限后不是热点的数据就会被删除,剩下的就是热点数据
Redis分布式锁
-
乐观锁:watch命令监听数据,若在事务执行中发生变化就停止,进入下一次循环直到监听的数据没有发生变化
-
悲观锁:使用setnx命令来抢锁,抢到锁的就可以执行数据操作,在用expire给锁加一个过期时间防止忘记释放,在最后还是要删除锁防止死锁
-
悲观锁问题:设置锁和设置有效期不符合原子性;当抢到锁之后到设置过期时间之前出现进程以为carsh或者重启服务了,就会出现死锁
-
解决方案:setnx命令替换成set命令,符合原子性,同时设置
-
set 命令:
set key value [EX seconds] [PX milliseconds] [NX|XX] EX seconds:设置失效时长,单位秒 PX milliseconds:设置失效时长,单位毫秒 NX:key不存在时设置value,成功返回OK,失败返回(nil) XX:key存在时设置value,成功返回OK,失败返回(nil)
# 需求:使用redis 悲观锁实现秒杀功能(防止超卖) from redis import StrictRedis, WatchError # 1、创建客户端 redis_client = StrictRedis(password=4988, decode_responses=True) # 2、构建锁 key = 'order:lock' while True: try: # 3、抢锁,成功返回True反之返回False # Redis的 setnx 命令是当 key 不存在时设置key,但不能设置过期时长。可用set命令代替 # lock = redis_client.setnx(key, 1) lock = redis_client.set(name=key, value=1, ex=5, nx=True) # print(lock) # 防止忘记删除锁资源,给锁资源添加过期时长 -- 防止出现死锁 # redis_client.expire(key, 5) # 4、读取库存数量 count = redis_client.get('count') # 争夺到锁资源后允许对redis进行操作 if lock: if int(count) > 0: redis_client.decr('count') print("下单成功") else: # 无库存 print("商品已售罄") redis_client.delete(key) break except WatchError: print("数据被修改,正在重试") continue -
Redis 缓存更新、缓存雪崩、穿透和击穿
-
缓存更新:mysql和redis是两个独立的系统,当两个用户并发更新同一个数据时,可能因为网络等延迟问题,出现mysql和redis的数据不一致的问题
- 解决方案1: 悲观锁(redis-setnx)使用消息队列顺序执行
- 缺点:并发能力差,将并发操作变成串行执行
- **解决方案2: **先更新写入mysql的同时删除redis缓存,下一次请求redis缓存时会进行数据回填,此时数据一致
- 主要用于数据对象(少的数据进行直接删除)
- 数据集合可以考虑更新缓存(集合的数据量大查询成本高,不易直接删除,只能采用更新部分数据)
- 解决方案1: 悲观锁(redis-setnx)使用消息队列顺序执行
-
缓存穿透:缓存只是为了缓解数据库压力而添加的一层保护层,当从缓存中查询不到需要的数据时就会到数据库中去查。当频繁访问缓存中没有的数据就会导致缓存穿透
-
解决方案1:对于数据库中不存在的数据,也对其在缓存中设置默认值Null,试请求不会到达DB层
- 为避免占用资源,一般会设置较短的过期时间
-
解决方案2:可以设置一些过滤规则
-
如布隆过滤器(bloomfilter),在应用服务前,将所有可能的值录入过滤器,如果不包含直接返回None,会有误杀概率
-
布隆过滤器(bloomfilter)的缺点:
- 存在误判:可以在 bloom filter中存储的是黑名单,可以建立一个白名单来存储容易误判的元素
- 删除困难
-
代码示例:在请求redis之前就进行验证
import pybloomfilter def get_user_info(user_id): # 创建过滤器对象 # 参数:布隆过滤器容器大小,误差值,文件名 filter = pybloomfilter.BloomFilter(10000, 0.001, "world bloom") # 添加数据 id_list = [i for i in range(1, 100)] # filter.update(('bj', 'sh', 'ss')) filter.update(id_list) # 判断是否包含 if user_id not in filter: print('不包含此id') return None print('包含')
-
-
-
缓存雪崩:如果大量的key设置同一过期时间,导致大量key过期,而正在此时有大量客户涌入,轻则出现redis卡顿,重则出现缓存雪崩
- 解决方法1:在key的过期时间后加上一个随机值,使得过期时间分散一些
- 解决方法2:采用多级缓存,不同级别缓存设置的超时时间不同,即使某个级别缓存都过期,也有其他级别缓存兜底
redis 的消息通知机制-- 实现订单倒计时
/截图/redis 消息通知.png)
/截图/redis 消息通知 订单倒计时方案.png)
注意:在开启消息通知时,需要修改配置文件允许键的通知,官网没有该命令
/%E6%88%AA%E5%9B%BE/redis%E9%94%AE%E7%A9%BA%E9%97%B4%E9%80%9A%E7%9F%A5.png)

浙公网安备 33010602011771号