Redis 其他知识点
NoSQL数据库(非关系型数据库)
特点
- 方便扩展,数据之间没有关系
- 大数据量高性能(Redis 一秒写 8 万次, 读取 11 万, NoSQL 的缓存记录级,性能较高)
- 数据类型是多样型的, 不需要事先设计数据库, 随取随用
Redis 特性
- 多样的数据类型
- 持久化
- 集群
- 事务
- …
Redis 内存划分
- 数据, 是主要部分, 这部分占用内存会统计在used_memory中
- 进程本身运行需要的内存, 入代码,常量池等,这部分内存大约几兆,不会统计在used_memory中,除了主进程外,创建的子进程也会占用内存,如AOF, RDB重写是创建的子进程
- 缓冲内存, 包括客户端缓冲区, AOF缓冲区等.会统计在used_memory中
- 内存碎片. 在分配,回收物理内存过程中产生的.如对数据的更改频繁, 而且数据之间大小相差较大,可能导致释放的空间在物理内存中没有被释放,但Redis又无法有效利用,这就形成了内存碎片,不会统计在used_memory中.如果内存碎片已经很大,可以通过安全重启的方式减小内存碎片.因为重启后,Redis重新从备份文件中读取数据,在内存中排,为每个数据重新选择合适的内存单元,减小内存碎片
Redis 和 Memcached 的区别
- Redis支持复杂的数据结构
- Redis原生支持集群模式,而 Memcached 没有原生的集群模式,需要依赖客户端来实现往集群中分片写入数据
- 性能对比
- Redis只使用单核,而Memcached可以使用多核,所以平均每个核上Redis存储小数据是比Memcached性能更好,在100K以上是,Memcached性能要高与Redis
Redis 单线程模型效率高的原因
- 纯内存操作
- 核心是基于非阻塞的IO多路复用机制
- C语言实现.执行速度相对更快
- 单线程反而避免了多线程的频繁上下文切换,预防了多线程可能产生的竞争问题
Redis 单线程模型
基于 Reactor 模式来设计开发的一套高效的事件处理模型
通过 IO 多路复用程序来监听客户端的大量连接(多个socket), 会将感兴趣的事件及读写操作注册到内核中并监听每个事件是否发生.
IO 多路复用技术的使用让 Redis不需要额外的创建多余线程来监听客户端的大量连接,降低资源的消耗
当被监听的连接准备好执行连接应答,读取,写入,关闭等操作时,与操作对应的文件事件就会产生,这是文件事件处理器就会调用之前关联好的事件处理器(连接应答处理器,命令请求处理器,回复处理器等)来处理这些事件
Reds 6.0 开始引用多线程
在某些方面,单线程已经不具有优势了,读写网络的系统调用在Redis执行期间占用率大部分CPU时间,如果把网络读写做成多线程的方式会对性能有很大提升
Redis 的多线程主要是为了提高网络的 IO 读写性能,执行命令时仍然是单线程
Redis过期策略
- 定期删除
- Redis 默认每隔100ms 就随机抽取部分设置了过期时间的key, 检查是否过期,过期就删除
- 惰性删除
- 获取key的时候,如果此时key已经删除,就不会返回任何东西
内存淘汰机制
- 当内存不足以容纳新数据时,新写操作会报错
- 移除最近最少使用的key,最常使用
- 随机移除某个key
- 在设置了过期时间的key中移除最少使用的key
- 在设置了过期时间的key中随机移除某个key
- 在设置了过期时间的key中,移除更早过期时间的key
Redis 事务
- Redis 事务没有隔离级别的概念
- Redis 单条命令保证原子性,但是事务不保证原子性.
- 编译型异常(代码有问题! 命令有错!) ,事务中所有的命令都不会被执行!
- 运行时异常(比如 1/0), 如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常!
- multi # 开启事务
- exec # 执行事务
#编译型异常(代码有问题! 命令有错!) ,事务中所有的命令都不会被执行!
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec # 执行事务报错!
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k5 # 所有的命令都不会被执行!
(nil)
# 运行时异常(1/0), 如果事务队列中存在语法性,
#那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常!
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 # 会执行的时候失败!
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range # 虽然第一条命令报错了,但是
依旧正常执行成功了!
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2
"v2"
Redis 监控 Watch (可以实现乐观锁)
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监视 money 对象
OK
127.0.0.1:6379> multi # 事务正常结束,数据期间没有发生变动,这个时候就正常执行成功!
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
127.0.0.1:6379> watch money # 监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
127.0.0.1:6379> exec # 执行之前,另外一个线程,修改了我们的值,这个时候,就会导致事务执行失
败!
(nil)
修改失败,解锁后获取最新值在进行即可
缓存雪崩及解决方案
所有缓存都同一时间失效,导致请求都直接访问数据库
- 事前: Redis高可用, 主从 + 哨兵, Redis集群,避免全盘崩溃
- 事中: 本地ehcache 缓存 + hystrix限流降级, 避免MySQL被打死
- 事后: Redis持久化,重启后,自动从磁盘加载数据,快速恢复缓存数据
- 缓存失效时间加上一个随机数时间,避免同一时间大批量缓存失效
缓存穿透及解决方案
黑客恶意攻击,缓存查询不到,每次都去查询数据库,如查询ID为 -1 的商品
判断条件是否正常,如ID已经是设为大于 0 的自增数,当请求给的为小于0的数直接返回空,其他请求查询数据库,没查询到结果就放到缓存中,设置一个过期时间,下次有相同的key来访问的时候,直接从缓存中取出来, 还可以使用布隆过滤器拦截
缓存击穿
某个热点数据访问非常频繁,在这个key失效瞬间,大量请求击穿,直接请求数据库
- 若缓存的数据基本不发生变更,可以设置为永不过期
- 若更新不频繁,且刷新的流程较少,可以使用分布式锁保证仅少量请求能请求数据库,其余线程等待锁释放后直接访问缓存
- 若缓存数据更新频繁且刷新流程较长,利用定时线程在缓存过期前主动更新缓存
布隆过滤器
概念
实际上是一个很长的二进制向量和一系列随机映射函数
可以用于检索一个元素是否在一个集合中.
优点是空间效率和查询时间都远远超过一般的算法
缺点是有一定的误识别率和删除困难
总结: 布隆过滤器说某个元素存在,小概率会误判, 布隆过滤器说某个元素不存在,那么这个元素一定不存在, 可用于解决缓存穿透问题
原理
当一个元素被加入到集合时
- 使用布隆过滤器的哈希函数对元素值进行计算,得到哈希值
- 根据得到的哈希值,在位数组中把对应下标的值置位 1
当需要判断一个元素是否存在时
对给定元素在此进行相同的哈希计算
得到值之后判断为数组中的每个元素是否都为1, 如果都为1,那么就说说明这个值可能在布隆过滤器中,如果存在一个值不为 1 , 则说明该元素肯定不在
保证缓存与数据库的双写一致性
- 读的时候,先读取缓存,没有的话在读取数据库,然后放入缓存中
- 更新的时候,先更新数据库,然后在删除缓存
Redis并发竞争问题
多客户端同时并发写一个key,本来先到的数据后到了,导致数据版本错误,或者多个客户端同时获取一个key,修改值后在写回去,顺序错了,数据也就错了
使用zookeeper的分布式锁,每个系统通过获取分布式锁,确保同一时间只有一个系统能操作某个key,别人不允许读和写.
每次写之前都先判断一下当前这个value的时间戳是不是比缓存的value时间戳要新,如果是,那就可以写入,否则不能用旧数据覆盖新数据
Redis分布式锁
利用setnx + expire (错误做法)
setnx 和 expire 是分开的两步操作,不具有原子性,执行完第一条命令后程序异常重启锁将无法过期
使用Lua脚本,包含 setnx + expire 指令
Lua脚本可以保证原子性
使用 set key value [EX seconds] [PX milliseconds] [NX|XX](正确做法)
- EX seconds: 设置过期时间,单位秒
- PX milliseconds : 设置过期时间, 单位毫秒
- NX : 仅当key不存在是设置值
- XX : 仅当key存在是设置值
- 锁超时问题, 执行完业务逻辑后超过了锁过期时间,可能会删除其他人的锁,需要锁时间续期
- 锁丢失问题.主节点拿到锁, 但是还没同步到从节点,主节点故障,从节点升级为主节点后导致锁丢失
使用Redission框架的分布式锁 (主要使用)
主要实现了对 RedLock 算法的封装
RedLock 原理
- 获取当前Unix时间, 毫秒为单位
- 依次尝试从N个实例, 使用相同的key和随机值获取锁. 当向 Redis 请求获取锁时, 客户端应该设置一个网络连接和响应超时时间, 这个超时时间应该小于锁的失效时间. 如锁自动失效时间为10秒, 则超时时间应该在5-50毫秒之间, 可以避免服务器端Redis已经挂掉的情况下, 客户端还在死死等待响应结果, 如果服务端还没有在规定时间内响应, 客户端应该尽快尝试向另一个Redis实例请求获取锁
- 客户端使用当前时间减去开始获取锁时间(步骤一), 得到获取锁使用的时间, 当且仅当从大多数(N/2 + 1)的Redis节点都获取到锁, 并且使用的时间小于锁失效时间时, 锁才算成功
- 如果获取到了锁, key 的真正有效时间等于获取锁使用的时间
- 如果获取锁失败, 客户端应该在所有的Redis实例上进行解锁.防止某些节点获取到锁但是客户端没有得到响应而导致接下来一段时间内不能重新获取锁
保证可重入性原理:
- 将锁的value 设置为 JSON字符串, 在其中加入当前线程id和count变量
- 当count变量为0时, 表示当前分布式锁没有被线程占用
- 当count变量大于 0, 且线程id不是当前线程, 表示分布式锁被其他线程占用
- 当count变量大于 0 且线程id是当前线程, 则表示当前线程已经拿到锁,不需要阻塞, 可以直接重入, 并将count变量加 1. 释放锁时则减 1