文章目录
1.为什么要使用缓存
-
缓解关系数据库并发访问的压力:热点数据
-
减少响应时间:内存IO速度比磁盘快(从数据库中读取数据一般十几毫秒,从内存中读取数据一般十几微妙)
-
提升吞吐量:Redis等内存数据库单机就可以支撑很大的并发
2.常见的内存缓存工具有Redis和Memcached
3.简述Redis常用数据类型和使用场景
数据类型 | 应用场景 |
---|---|
String(字符串) | 计数器,分布式锁 |
List(链表) | 消息队列,任务队列 |
Hash(Hash表) | 用户信息 |
Set(集合) | 去重 |
ZSet(有序集合) | 排行榜 |
4.Redis内置实现
类型 | c底层实现 |
---|---|
String | int或者sds(simple dynamic string) |
List | ziplist或者double linked list双向链表功能 |
Hash | ziplist或者hashtable |
Set | intset或者hahstable |
SortedSet | skiplist跳跃表 |
注意
1.这些数据结构操作的时间和空间复杂度是多少,深入学习参考 <<Redis设计与实现>>
5.Redis持久化方式
持久化方式 | 定义 |
---|---|
RDB(Redis Database Backup file) | 将内存数据保存到磁盘的二进制文件dump.rdb中( 主从复制就是基于RDB持久化功能实现),速度更快,一般用作备份,也称之为基于快照的持久化 |
AOF(append Only File) | 每一个写命令追加到appendonly.aof中,可以最大程度的保证redis数据安全,类似于mysql的binlog |
-
RDB
dir /data/6379 #定义持久化文件存储位置 dbfilename dbmp.rdb #rdb持久化文件
-
AOF
appendonly yes appendfsync always 总是修改类的操作 everysec 每秒做一次持久化 no 依赖于系统自带的缓存大小机制
6.Redis事务
- 将多个请求打包,一次性,按序执行多个命令的机制
- redis通过multi,exec,watch等命令实现事务功能
- redis-py
pipline=conn.pipeline(transaction=True)
7.redis如何实现分布式锁
背景:分布式系统在共享资源时为保证数据的一致性
- 使用setnx(key,value,expire)实现加锁(互斥锁)
- 锁的value值可以使用一个随机的uuid或者特定的命名
- 释放锁的时候,通过uuid判断是否是该锁,是则执行delete释放锁
支持超时时间参数
import uuid
import math
import time
def acquire_lock_with_timeout(conn, lock_name, acquire_timeout=3, lock_timeout=2):
"""
基于 Redis 实现的分布式锁
:param conn: Redis 连接
:param lock_name: 锁的名称
:param acquire_timeout: 获取锁的超时时间,默认 3 秒
:param lock_timeout: 锁的超时时间,默认 2 秒
:return:
"""
identifier = str(uuid.uuid4())
lockname = f'lock:{lock_name}'
lock_timeout = int(math.ceil(lock_timeout))
end = time.time() + acquire_timeout
while time.time() < end:
# 如果不存在这个锁则加锁并设置过期时间,避免死锁
if conn.set(lockname, identifier, ex=lock_timeout, nx=True):
return identifier
time.sleep(0.001)
return False
def release_lock(conn, lock_name, identifier):
"""
释放锁
:param conn: Redis 连接
:param lockname: 锁的名称
:param identifier: 锁的标识
:return:
"""
unlock_script = """
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
"""
lockname = f'lock:{lock_name}'
unlock = conn.register_script(unlock_script)
result = unlock(keys=[lockname], args=[identifier])
if result:
return True
else:
return False
深入思考:如果Redis单个节点宕机,如何处理?还有其他业界的方案实现分布锁吗
参考:
8.Redis三种常见的缓存模式
- 旁路模式(cache side pattern)
- 读写穿透模式(read/write through pattern)
- 异步写入模式(write behind pattern)
1.旁路模式
-
写:更新db->删除cache
-
读:查cache
- 存在直接还回
- 不存在则查db,将db数据直接返还给客户端,然后再更新给cache
-
应用场景:使用最多的模式,适用于对数据一致性要求不高,读多写少的场景,减轻读的压力,例如新闻资讯类系统
2.读写穿透模式
-
写:查cache
- 存在更新cache,更新db
- 不存在更新db
-
读:查 cache
- 存在直接还回
- 不存在则查询db,将db数据更新给cache,然后通过cache返还给客户端
-
应用场景:适用于对数据一致性要求高的场景,电子商务系统金融交易系统
3.异步写入模式
- 写:更新缓存后直接响应,之后将缓存中的数据异步写入数据库中
- 读:和旁路缓存模式一样
- 应用场景:使用于对性能要求较高,数据一致性要求不高的场景,写多读少的模式,减轻系统写入读的影响,日志处理系统,大数据处理平台
旁路模式的缺陷
- 首次请求的数据一定不存在cache中
- 可以提前将热点数据存入到缓存中
- 写操作较多的场景,缓存会被频繁删除,影响缓存命中率
- 添加分布式锁保证同时更新缓存和数据库
- 对缓存添加较短的过期时间
旁路模式写入操作时为什么不先删除缓存再更新数据库
- 保证数据一致性,即使存在数据不一致但概率相对较低
9.Redis三种常见的问题
- 缓存穿透
- 缓存击穿
- 缓存雪崩
1.缓存穿透
描述: 大量要访问的数据既不在Redis 缓存中,也不在数据库
原因: 缓存和数据库中都没有要访问的数据
解决方案
- 对于没查到返回为None的数据也缓存或者设置缺省
- 对入口请求合法性检查
- 布隆过滤器
- 插入数据的时候删除相应缓存,或者设置较短的超时时间
2.缓存击穿
描述: 某个访问非常频繁的热点数据,缓存中没有数据,访问该数据的大量请求,一下子都发送到数据库,导致数据库压力激增
原因: 热点数据过期失效
解决方案:
- 不给热点数据加过期时间
- 分布式锁:获取锁的线程从数据库拉数据更新缓存,其他线程等待
- 异步后台更新:后台任务针对过期的key自动刷新
3.缓存雪崩
- 描述:缓存中有大量的数据同时过期导致在Redis缓存无法处理,应用将大量请求发送到数据库层,导致数据库层的压力激增
- 原因:
- 缓存中有大量数据同时过期,导致大量请求数据库
- 实例宕机
- 解决方案:
- 多节缓存:不同级别的key设置不同的超时时间
- key的超时时间随机设置,防止同时超时
- 架构层:提升系统可用性.监控,报警完善
12.实现发布/订阅
订阅者
import time
from redis import StrictRedis
s = StrictRedis(host='', port=6379, password='')
sub = s.pubsub()
sub.subscribe('FM101', 'FM102')
sub.subscribe('*ython1', '2python*') # 通配符不生效
while True:
time.sleep(1)
msg = sub.parse_response()
print("msg:%s" % msg)
发布者
from redis import StrictRedis
r = StrictRedis(host='', port=6379, password='')
res1 = r.publish('FM101', 'hello 1')
res2 = r.publish('FM102', 'hello 2')
res3 = r.publish('python1', 'python1')
res4 = r.publish('2python2', 'python2')
print(res1) # 返回订阅者数量
print(res2) # 返回订阅者数量
print(res3) # 返回订阅者数量
print(res4) # 返回订阅者数量
13.Redis的python基本操作
from redis import StrictRedis
r = StrictRedis(host='', port=6379, password='')
r.ping() # 运行测试命令
r.select(1) # 切换数据库
# 1.String
# mset,mget
r.set('age', 23, ex=100, nx=True)
r.get('age')
# mset,mget
r.mset({'k3': 'v3', 'k4': 'v4'})
r.mget('k1', 'k2', 'k3') # [None, None, b'v3']
# append
r.get('age') # b'17'
a = r.append('age', 6) # b'176'
a.decode('utf-8') # '176
# 2.keys
# key
r.keys() # [b'age', b'k4', b'k3']
r.keys('k*') # 查看包含k的键
# exists
r.exists('age') # 1:表示存在 0表示不存在
# type
r.type('age') # b'string'
# delete
r.delete('k3', 'k4') # 正数:表示删除的数量 0表示不存在
# expire
r.expire('k3', 6) # 将k3这个键设置为6秒过期
# getrange
r.get('k4') # b'v4'
r.getrange('k4', 0, 1) # b'4'
# key
r.ttl('k4') # 查看键k4的有效时间
# 3.hash
r.hset('person', 'name', 'jason')
r.hset('person', 'age', 18)
r.hget('person', 'age')
r.hgetall('person')
r.hvals('person')
r.hmset('person3', {'name3': 'jason', 'age3': 16})
r.delete('person1')
r.hdel('person3', 'name3')
# 4.list
r.lpush('a1', 'a', 'b', 'c') # 在左侧插⼊数据
r.rpush('a1', 0, 1) # 在右侧插⼊数据
r.lrange('a1', 0, -1)
r.lset('a1', 0, 'z')
r.lrem('a1', -2, 'b') # 从a1列表右侧开始删除2个b
# set:集合没有修改操作
r.sadd('name', 'jason1', 'jason2')
r.smembers('name')
r.srem('name', 'json1')
r.srem('name')
# zset:有序集合没有修改操作,通过权重将元素从⼩到⼤排序
r.zadd('name2', {'jason1': 4, 'jason2': 5})
r.zrange('name2', 0, -1)
r.zrangebyscore('name2', 4, 5)
r.zscore('name2', 'jason1')
r.zremrangebyscore('name2', 4, 5)
# 管道
pipe = r.pipeline()
pipe.set('a1', '11')
pipe.set('a2', '12')
pipe.execute()
pipe.watch() # 如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
pipe.multi() # 标记一个事务块的开始
参考: