Redis
数据类型
string
-
常用操作:
1. set key value 2. get key 3. mset key value [key value ...] 4. mget key [key ...] 5. set key value [NX | XX] [EX | PX | EXAT | PXAT | KeepTTL] # NX XX 分别表示键不存在时插入、键存在时插入 # EX PX EXAT PXAT 分别表示 以秒、毫秒、UNIX秒、UNIX毫秒为单位的过期时间 # KeepTTL 保存当前键的过期时间 6. getrange/setrange # 获取指定区间的值 -1表示全部 7. incr/decr/incrby/decrby # 自增、自减、增加指定数、减少指定数 8. append/strlen # 追加内容、字符串长度 9. setnx 10. setex 11. getset # 先get再set -
应用场景:
- 抖音直播点赞,点一下加一个
hash
-
常用操作:
1. hset/hget/hmset/hmget/hgetall/hdel 2. hlen # 获取某个key的字段的数量 3. hkeys / hvals # 获取key中的的所有field和value 4. hsetnx -
应用场景:
- 购物车设计
list
-
常用操作:
1. lpush/rpush/lpop/rpop rpoplpush 2. lrange # 获取指定范围内的元素 3. lindex/llen 4. lrem key 数字N valueX # 删除N个值为valueX的元素 5. ltrim key start end # 只保留start 到 end 内的元素 6. lset key index value # 在index位置插入key-value -
应用场景:
- 微信公众号的订阅消息
set
-
常用操作:
1. sadd member [member ....] 2. smembers key 3. sismember key member # 判断元素是否在集合中 4. srem key member [member ...] # 删除元素 5. scard key # 获取key中的元素个数 6. # 集合运算 6.1 sdiff # 差 6.2 sunion # 并 6.3 sinter # 交 -
应用场景:
- 朋友圈点赞
- 抽奖小程序
zset | sortedSet
-
常用操作:
1. zadd key score member [sroce member ...] 2. zrange key start stop 3. zscore key member # 获取元素的分数 4. zcard key # 获取集合中的元素数量 5. zrem key # 删除元素 6. zrank key value # 获取下标值 7. zcount key min max # 获取min-max分数内的元素个数 -
应用场景:
- 根据商品销量对商品进行排序
bitmap
-
介绍:只有0和1状态的二进制位的bit数组
-
常用操作:
1. setbit key offset value # 给指定key的值的第offset复制val 2. getbit key offset # 获取指定key的第offset位 3. bitcont key start end # 获取key中[start, end]为1的数量 4. bitop operation destkey key # 对不同的二进制存储数据进行位运算(and,or,not,xor) 5. strlen key # 统计字节数占用 -
应用场景:
- 月签到、年签到、周签到、日活跃、周活
- 一年中的登录次数
- 电影、广告被点击播放量
hyperLogLog
-
介绍:基数统计,去重,存在0.81%的误差,不是集合也不存储数据,只记录数量,没有具体内容
-
常用操作:
1. pfadd key element [element ...] 2. pfcount key [key ...] # 获取key中元素的基数估算值 3. pfmerge destkey sourcekey [sourcekey ...] # 合并多个HyperLogLog 为一个HyperLogLog -
应用场景
- UV用户访问量
- PV页面浏览量
- DAU 日活跃量
- MAU 月活跃量
Geo
-
常用操作
1. geoadd key 经度 纬度 Name [经度 纬度 Name ...] 2. geopos key name [name ...] # 获取经纬度 3. geodist key Name1 Name2 单位 # 以长度单位返回Name1 与 Name2的距离 4. georandius key 经度 维度 num 单位 withdist withcoord count 10#以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素 -
应用场景:
- 酒店定位
- 外卖定位
key
-
常用操作:
keys * # 查看当前库的所有key type key # 当前key的类型 del key # 删除key unlink key # 非阻塞删除 ttl key # 查看还有多少秒过期 -1 表示不过期, -2表示已过期 expire key 秒钟 # 为key设置过期时间 move key dbindex [0-15] # 将当前数据库的key移动到给定的数据库db中 select dbindex # 切换当前数据库 dbsize # 查看当前数据库key的数量 flushdb # 清空当前库 flushall # 清空全部库
持久化
RDB
-
原理:全量快照
-
以快照的形式,保存数据库中的数据;以快照的形式,恢复数据库中的数据
-
触发
-
手动触发:save和bgsave命令
-
自动触发
- 配置文件中默认的快照配置
- 执行flushall/flushdb命令
- 执行shutdown命令,且没有开启AOF
- 主从复制时,主节点自动触发
-
-
-
常用操作
1. 配置文件中的 save 2. 自定义dump文件的保存目录和文件名 3. 手动触发RDB的 bgsave和save命令 4. 修复rdb文件-- redis-check-rdb filename.rdb -
优势
- 适合大规模数据恢复
- 适用于定时备份
- 对数据完整性和一致性要求不高
- RDB文件的加载速度快于AOF文件的加载
-
劣势
- 部分数据会丢失
- 全量同步的话,存在I/O性能问题
AOF
-
原理
-
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来,只许追加文件,不可以改写文件,redis启动指出会读取改文件重新构建数据,redis重启的话,就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复
-
默认不开启AOF,配置文件中的AppendONLY 改为yes
-
工作流程
- Client请求写数据
- 先到达Redis,然后将写指令写入到AOF缓冲区,当AOF缓冲区中的指令达到一定数量后写会AOF文件
- AOF缓冲区根据是那种写回策略将指令写入到AOF文件中
- 随着AOF的碰撞,AOF会进行压缩也就是AOF重写机制
- 当Redis重启时,从AOF文件载入数据
-
-
写回策略
- Always 已知直接的写回
- everysec 每秒协会--默认
- no 不写回,有操作系统决定是否写回
-
重写机制:只保留最小指令集(最终指令)
-
配置文件:①相较于上次重写增长100% 且 ② 重新时满足文件大小 64mb
-
自动触发:满足配置文件要求后,直接rewrite
-
手动触发:使用bgrewriteaof指令
-
原理:
- 创建一个“重写子进程”,这个子进程会读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中
- 主进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。
- 当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中
- 当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中
- 重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件
-
-
常用操作
- 开启AOF--
appendonly yes - 修改策略--
appendsync everysec - aof文件保存路径 --
appenddirname "appendonlydir" - 文件修复命令 --
redis-check-aof -- fix
- 开启AOF--
-
优势
- 更好的保证数据不丢失、性能高、可做紧急恢复
-
劣势
- 相同数据集的数据,aof文件大小比rdb大,恢复速度慢与rdb(I/O)
RDB+AOF
-
同时开启AOF和RDB时,重启Redis时,只会加载AOF,不会加载RDB文件
-
设置aof-use-rdb-preamble的值为 yes
-
RDB镜像做全量持久化,AOF做增量持久化
先使用RDB进行快照存储,然后使用AOF持久化记录所有的写操作,当重写策略满足或手动触发重写的时候,将最新的数据存储为新的RDB记录
事务
定义和特性
- 定义:一个队列,一次性,顺序性,排他性的执行一系列命令
- 单独的隔离操作,没有隔离级别,不保证原子性,排他性
常用命令
discard取消事务exec执行事务块内的命令multi标记一个事务的开始UNwatch取消watch命令对所有key的监视watch key [key ...]监视一个或多个key
操作
- 正常执行:multi、exec
- 放弃事务:discard、multi
- 与传统事务不同,不一定一起成功一起失败
- watch监控
执行过程
- 开启:以Multi开始一个事务
- 入队:将多个命令入队到事务中,接到命令不会立即执行,放入到等待队列中
- 执行:Exec触发执行事务
管道pipeline
定义
- pipline为了解决RTT往返时间,仅仅是将命令打包,一次性发送
- 批处理命令
与原生批处理的对比
- 原生批处理是原子性的,pipeline不是原子性的
- 原生批处理命令一次只能执行一种命令,pipeline支持批量执行不同命令
- 原生批处理命令是服务端实现,pipeline是需要服务端和客户端共同完成
与事务的对比
- 事务具有原子性(具有不是保证),pipeline不具有原子性
- 事务一条条发送命令,pipeline批量发送
- 事务会阻塞其他命令,pipeline不会阻塞
主从复制和哨兵
主从复制
-
实现读写分离、容灾恢复、数据备份、水平扩容支持高并发
-
从库搭建到主库上
-
常用一主二从
-
可以使用命令
-
从机只能读,不可写
-
三个命令
- replicaof 主库IP 主库端口 配从库不配主库
- slaveof 新主库IP 新主库端口 改换门庭
- slaveof no one 自立为王
-
从库延迟上线,也会先赋复制一份数据,再后续跟随
-
主库宕机,从库不动,不会上位需要借助哨兵
-
主库重启,从库会自动再次连接上 -- 没有哨兵机制
-
从库重启后,master还是原master
复制原理和工作流程
-
slave启动,同步
- slave启动后,向master发送一个sync命令
- slave首次连接master后,一次完全同步全量复制,自身数据被覆盖
-
首次连接,全量复制
- master接收到syn命令后,保存RDB,并将新命令缓存起来,最后将缓存命令和RDB文件发送给slave
- slave接收到后,进行全量复制,完成初始化
-
心跳持续,保持通信:发送心跳包ping
-
进入平稳,增量复制
-
从机下线,重新续传:offset
哨兵sentinel
- 主从监控:监控主从redis运行是否正常
- 消息通知:哨兵可以将故障转移的结果发送给客户端
- 故障转移:主库宕机,从库上位
- 配置中心:客户端通过连接哨兵来获得当前Redis的主节点地址
哨兵运行流程和选举原理
-
下机或宕机
- 主观下机:单个哨兵主观检测到master宕机状态,ping心态包无回复
- 客观下机:多个哨兵达成一致意见认为master宕机
-
选出新主库
- 当客观下线时,选举出一个兵王,由兵王完成故障转移
- 兵王的选择算法:Raft算法
-
兵王开始进行故障切换,选出新的master
- 从从库中选出一个成为新主库:①健康状况;②配置的优先级;③offset偏移量最大的从库;④字典序
- 其他从库跟随新主库:sentinel leader 发送命令使其他从库跟随主库
- 旧主库上线后,也要跟随新主库
集群Cluster
- 不保证强一致性,可能会丢掉一部分数据或者命令 ---- AP
作用
- master支持多个slaver,因此可以实现①读写分离;②支持数据的高可用;③支持海量数据的读写存储操作
- Cluster自带sentinel的故障转移机制,内置了高可用的支持,无需再使用哨兵
- 客户端与Redis的节点连接,不再需要连接集群中的所有节点,只需要任意连接集群中的一个可用结点即可
- 槽位slot负责分配到各个物理服务结点,由对应的集群负责维护节点、插槽slot和数据之间的关系
分片slot原理
-
引入hash槽slot,集群中有16384个slot,每个key通过crc16效验后对16384取模来决定放置到那个slot,集群中的节点各负责一部分slot
-
为什么是16484=2^14,而不用2^16次幂呢?
- 集群不可能扩展到10000+,2^14次幂足够了
- 心跳包大小不同,若是2^16的话,心跳包的消息头为8kb;而16384位2kb
- 槽位越小,节点少的情况下,压缩比高,容易传输
-
hash分区算法:
- 哈希取余
- 一致性哈希算法
- 哈希slot分区:用crc16进行哈希取模
主从扩容
- 当添加新的节点时,会进行重新分派slot
主从缩容
- 当某个节点宕机后,会对宕机的slot进行重新分配给其他节点(一个或多个)
redis集群有哪些种类
- 主从复制集群,手动切换
- 带有哨兵的HA的主从复制集群
- 客户端实现路由索引的分片集群
- 使用中间件代理层的分片集群
- redis自身实现的Cluster分片集群
BigKey
定义
- string类型在10kb以内,hash、list、set、zset元素不超过5000
keys *
- 通过修改
rename- command keys ""禁用keys */flushdb/flushall等命令 scan cursor [match pattern] [Count count]
非阻塞命令
- string 使用
unlink代替del - hash 使用
hscan + hdel代替hdel - list 使用
ltrim一点点删 - Set 使用
sscan + srem - zset 使用
zscan + zremrangebyrank
一致性或最终一致性
双写一致性
- 如果redis有数据,则mysql必须有数据,且redis需要与mysql数据相同
- 如果redis没有数据,则mysql中必须有最新的数据
更新策略
-
先更新数据库,再更新缓存
- 问题:redis更新失败,造成不一致
-
先更新缓存,再更新数据库
- 问题:数据库更新失败,保底的redis,不合适
-
先删除缓存,再更新数据库
- 问题:先删除redis,更新数据库时存在网络延迟,线程获取到mysql的旧值,写回到redis,结果数据库更新完后,redis和数据库数据不一致
- 解决方案:延时双删策略 --- 看门狗WatchDog
-
先更新数据库,在删除缓存
-
问题:redis删除失败,导致数据库和redis不一致
-
解决方案:
- 双检加锁策略
- 中间件--canal
-
缓存问题
缓存击穿
-
原因:热点Key过期,导致大连请求发送给数据库
-
解决方案:
- 设置长点的过期时间,或者,不设置过期时间
- 双检加锁策略
缓存穿透
-
原因:数据库中没有该数据,redis中也没有该数据,黑客攻击
-
解决方案:
- 返回default空值,存在的问题是,空值不断增加,redis占用越来越大,这样还需设置过期时间
- bloom filter --- 阿里的或者guava或者redisson
缓存雪崩
-
原因:① 大量Key同时删除; ② 服务器宕机
-
解决方案:
- 设置不同的过期时间
- 使用redis集群或主从复制
- 服务降级:微服务的hystrix或者sentinel进行服务降级、限流、熔断
- 多缓存结合,本地缓存+redis缓存
缓存预热
- 提前加载数据
分布式锁
锁的必需条件
- 独占性:一个资源任何时刻只能有且一个线程持有锁
- 高可用:高并发下,不会丢失性能,不能在高并发下出现获取锁和释放锁失败
- 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作
- 不乱抢:只能自己加锁和自己释放锁,不能释放其他对象的锁
- 可重入:同一个节点的同一个线程如果获得锁后,可以再次获得这个锁
setnx
两种形式
setnx key value+expire key second两条命令,不能保证原子性set key value [EX seconds][PX milliseconds] [NX|XX]
setnx实现分布式锁
-
单机模式下,添加synchronized 或 ReentrantLock 时间基本业务锁;但是在分布式高并发下,由于锁只能锁当前微服务的模块,则还是会出现超卖现象
-
① 简单实现分布式锁:通过自旋代替递归(会出现StackOverflowError)重试 + setnx实现分布式锁(set获取锁,delete释放锁)
问题:当微服务宕机,加的锁就无法释放了,所有需要设计过期时间
-
② 宕机与过期+防止死锁:使用
set key value EX seconds加锁,并设置过期时间和保证原子性问题:当业务处理时间超过了过期时间,就会出现释放其他对象的锁
-
③ 防止误删key的问题:在释放锁时,进行判断是否是自己的锁
问题:既要判断,又要释放锁,两条命令语句,不能保证原子性
-
④ Lua脚本保证原子性:编写lua脚本实现判断和删除key的原子性
String luaScipt = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end"l; stringRedisTemplate.execute(new new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), uuidValue);问题:不可重入,只能解决有无锁的问题
-
⑤ 实现可重入锁+设计模式:
hset key filed val+ lua脚本保证原子性-
加锁:① 先判断redis分布式锁是否存在
exists key;②如果返回0,则说明不存在,再使用hset key filed 1进行加锁;如果返货1,则说明存在锁,需要使用hexists key 自己的ID判断是否是自己的锁;③如果是自己的锁,则自增1hincrby key filed 1;否则循环等待;④使用lua脚本保证原子性if reids.call('exist', keys[1] == 0) or redis.call('hexist', keys[1], argv[1] == 1) then -- 不存在锁 或 自己的锁 redis.call('hincrby', keys[1], argv[1], 1) -- 自增1,hincrby当不存在时,可以创建 redis.call('expires', keys[1], argv[2]) -- 设置过期时间 return 1 -- 返回true else return 0 -- 否则返回false end -
释放锁:有锁且为自己的锁时,才能释放-1,或减为0时删除。使用lua脚本保证原子性
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end -
引入工厂模式,实现Lock接口,重写lock(trylock)和unlock方法,实现代码复用
问题:不能自动续期
-
-
⑥ 自动续期:加锁时,开启一个定时任务进行递归续期或用job
private void renewExpire(){ // 续期的lua脚本 String script ="if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " + "return redis.call('expire',KEYS[1],ARGV[2]) " + "else return 0 " + "end"; // 开启一个定时任务线程 new Timer().schedule(new TimerTask(){ @Override public void run(){ // 判断是否续期成功 if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) { renewExpire(); // 递归调用 } } },(this.expireTime * 1000)/3); // 计时 每三分之一时间 执行一次 }
Redlock
- 问题:自研的分布式锁,存在单点故障问题:当Redis master 宕机,且没有写给Redis slaver时,则这个锁可能无法释放,当新的线程再请求资源时,仍可以获得锁,因此存在两个线程获得了锁。不能满足高可用,可能产生脏数据
RedLock的设计理念
-
Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用
-
采用N个Redis主节点,且这N个主节点是相互独立的,将锁放入到这N个主节点中
-
客户端只有在满足两个条件时,才可以加锁成功
- 客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;
- 客户端获取锁的总耗时没有超过锁的有效时间。
-
客户端如果加锁失败,则应该在所有的Redis实例中进行解锁
Redlock算法
为什么是奇数个实例
- 部署5个实例,当有三个成功时就可以断定成功,相反,当有两个失败就可判断失败
- 部署6个实例,当有四个成功时也可以断定成功,相反,当有两个失败就可判断失败
- 因此奇数部署服务器更少
Redisson
- redisson是Redloc在Java方向的落地实现
- Redisson是居于RLock可重入锁实现的 --- 满足了重入性
- 使用看门狗机制(守护线程每1/3时间检查一次)来检查所的超时时间(默认30秒)进行续期操作和防死锁
- 使用多Redis实例完善单点故障
- 使用leaseTime可以指定获得锁的超时时间(默认30秒),超时自动解锁
- 使用lua脚本保证原子性
源码分析
-
lock() --> tryAcquire() --> tryAcquireAsync() --> scheduleExpirationRenewWal(threadId) --> renewExpiration()
-
在lock(x,x,x)中设置了超时时间,默认30秒; 并调用tryAcquire()
-
在tryAcquire()中调用tryAcquireAsync() 进行加锁和续时
- 如果锁不存在,则通过hset设置值,并设置过期时间
- 如果锁已存在,并且锁的当前线程,则通过hincrby给数值增1
- 如果锁已存在,但并非本线程,则返回过期时间
-
在tryAcquireAsync()中调用renewExpiration()使用看门狗守护线程进行续时操作(续时也使用lua脚本实现)
-
解锁:使用lua脚本进行
-
如果释放的线程和已存在的线程不是同一个线程,返回null
-
否则,通过hincrby递减,先释放一次锁。若剩余次数大于0,则当前所是重入锁,刷新过期时间
若剩余次数小于等于0,则删除key,释放锁
-
MultiLock多重锁
RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);
RedissonMultiLock redLock = new RedissonMultiLock(lock1, lock2, lock3);
redLock.lock();
redis缓存淘汰策略
内存查看
- 配置文件中查看
maxmemory <bytes>或config get maxmemory或info memory - 在64位系统下,maxmemory默认设置为0,表示不限制redis内存使用
- 推荐配置:设置redis最大内存为物理内存的3/4
- 可以用命令临时配置
config set maxmemory bytes - 如果超出了内存配置的大小,报OOM Error
删除策略
-
立即删除:检测到过期,就删除
问题:对CPU不好,需要切换,但是可以快速是否空间 ----- 时间换空间
-
惰性删除:数据达到过期后,不做处理,等下次访问时进行删除
lazyfree-lazy-eviction=yes问题:对内存不好,当key大量过期且没有再次被访问,则出现内存占用 ----- 空间换时间
-
定期删除:每隔一段时间执行一次删除过期键操作并通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响,进行随机抽取来删除
问题:设置删除操作执行的时长和频率存在难点,也会存在浪费内存的情况
保底的淘汰策略--回收
- noeviction(默认):不会淘汰任何key,上限后返回OOM Error
- Allkeys-lru:对所有key使用lru算法优先删除最近不常使用的key
- Volatile-lru:对有过期时间的key使用lru算法优先删除最近不常使用的key
- Allkeys-lfu:对所有key使用lfu算法优先删除使用频率最少的key,相同时,删除最早的
- Volatile-lfu:对有过期时间的key使用lfu算法优先删除使用频率最少的key,相同时,删除最早的
- AllKeys-random:对所有key随件删除
- Volatile-random:对有过期时间的key随机删除
- Volatile-ttl:删除马上要过期的key
使用说明
- 所有key都是最近最经常使用的,用Allkeys-LRU
- 所有key的访问频次都差不多,用Allkeys-Random
- 对数据的过期时间足够了解,可以使用Volatile-ttl
config set maxmemory-policy xxxxx- 更改配置文件redis.conf
maxmemory-policy noeviction
LRU 最早未使用的
- LinkedHashMap
- HashMap + 双向链表
LFU 最少使用的
- 在LFU的基础上加入频次count
五大类型底层实现
- Key 一般是String类型的字符串对象
- Value是redis对象(RedisObject)
- ①bitmap -- string;②hyperloglog -- string;③Geo -- Zset; ④Stream -- Stream;⑤Bitfield -- 看key
字典、KV是什么
- redisSever --> redisDb --> dict --> dictht --> dictEntry --> 五大基本数据类型redisObject
- 每个键值对都有一个DictEntry,其中key执行String对象,value执行基本数据类型的RedisObject
String
-
SDS简单动态字符串
- 四个字段:
int len; int alloc; char flags; char buf[] - Len 表示SDS的长度,可以在O(1)时间内获得字符串长度
- alloc表示内存占用的字节长度,可以进行预分配空间算法
- buf表示字符串数组,真实数据
- 安全,C语言需要以\0结束无法识别\0后的数据,而SDS可以用len进行判断
- 四个字段:
-
strEncoding
- int:保存64位的整数,浮点数都会转换为字符串保存
- embstr:嵌入式的字符串:保存长度小于44字节的字符串
- raw:保存长度大于44的字符串
Hash
redis6
hash-max-ziplist-entries使用压缩列表保存时,哈希集合中的最大元素个数hash-max-zipilis-value使用压缩列表保存时哈希集合中单个元素的最大长度- 当同时小于上述条件,则使用ziplist存储,否则用hashTable
redis7
hash-max-listpack-entries使用紧凑列表保存时,哈希集合中的最大元素个数hash-max-listpack-value使用紧凑列表保存时哈希集合中单个元素的最大长度- 当同时小于上述条件,则使用listpack存储,否则用hashTable
List
redis6
- 在Redis3.0之前,list采用的底层数据结构是ziplist压缩列表+linkedList双向链表
- 在高版本的Redis中底层数据结构是quicklist(替换了ziplist+linkedList),而quicklist也用到了ziplist
- quicklist就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表
redis7
quicklist就是「双向链表 + 紧凑列表」组合,因为一个 listpack 就是一个链表,而链表中的每个元素又是一个压缩列表
Set
Redis用intset或hashtable存储set。如果元素都是整数类型,就用intset存储。如果不是整数类型,就用hashtable(数组+链表的存来储结构),key就是元素的值,value为null。
ZSet
redis6
zset-max-ziplist-entries使用压缩列表保存时集合中的最大元素个数zset-max-zipilis-value使用压缩列表保存时集合中单个元素的最大长度- 当同时小于上述条件,则使用ziplist存储,否则用skiplist
redis7
zset-max-listpack-entries使用紧凑列表保存时集合中的最大元素个数zset-max-listpack-value使用紧凑列表保存时集合中单个元素的最大长度- 当同时小于上述条件,则使用listpack存储,否则用skiplist
ZipList 压缩列表
结构
描述
- ziplist是一个经过特殊编码的双向链表,它不存储指向前一个链表节点prev和指向下一个链表节点的指针next而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面
优点
- ziplist为了节省内存,采用了紧凑的连续存储。(相较于链表,我们存储的实际数据的大小都没有两个指针大)
- ziplist是一个双向链表,可以在时间复杂度为 O(1) 下从头部、尾部进行 pop 或 push
- 头节点里有头节点里同时还有一个参数 len,因此获取链表长度时不用再遍历整个链表,O(1)
- 遍历比链表快,因为内存连续,直接使用偏移量就可以了
问题
- 新增或更新元素可能会出现连锁更新现象。(当前结点保存了前一个节点的长度,当前一个节点的长度从255变成256时,需修改当前的长度加1,可能导致后面都要加1)
- 不能保存过多的元素,否则查询效率就会降低,数量小和内容小的情况下可以使用。
ListPack 紧凑列表
- 和ziplist 列表项类似,listpack 列表项也包含了元数据信息和数据本身。不过,为了避免ziplist引起的连锁更新问题,listpack 中的每个列表项
- 不再像ziplist列表项那样保存其前一个列表项的长度。
结构
SkipList 跳表=有序链表+多级索引
- skiplist是一种以空间换取时间的结构。
- 由于链表,无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找,提取多层关键节点,就形成了跳跃表
- 由于索引也要占据一定空间的,所以,索引添加的越多,空间占用的越多
- 时间复杂度 O(log n) 空间复杂度 O(n)
- 适用于读多写少的场景
- 新增或者删除时需要把所有索引都更新一遍,为了保证原始链表中数据的有序性,我们需要先找到要动作的位置,这个查找操作就会比较耗时最后在新增和删除的过程中的更新,时间复杂度也是O(log n)
Redis为什么快
1 基于内存
- 本身基于内存,但是开启AOF就需要I/O进行日志的读写
2 优化的数据结构
3 单线程--I/O多路复用
- 多路:多个客户端连接(socket),值的是多条TCP连接
- 复用:单个进程来处理多条连接,使用单进程就能够实现同时处理多个客户端连接
- 异步非阻塞 --- Redis 服务采用 Reactor 的方式来实现文件事件处理器,因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型
- IO演变:Blocking-IO --> NoneBlocking-IO ---> IO多路复用
- 单线程无需考虑锁的复杂度,在操作数据上相对来说是原子性的
4 持久性和一致性
IO多路复用
BIO
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据,这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,BIO的特点就是在IO执行的两个阶段都被block了。
NIO
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,NIO特点是用户进程需要不断的主动询问内核数据准备好了吗?一句话,用轮询替代阻塞!
- 在NIO模式中,一切都是非阻塞的:
- accept()方法是非阻塞的,如果没有客户端连接,就返回无连接标识
- read()方法是非阻塞的,如果read()方法读取不到数据就返回空闲中标识,如果读取到数据时只阻塞read()方法读数据的时间
IO多路复用
IO multiplexing就是我们说的select,poll,epoll,有些技术书籍也称这种IO方式为event driven IO事件驱动IO。就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。可以基于一个阻塞对象并同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程,每次new一个线程),这样可以大大节省系统资源。所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select,poll,epoll等函数就可以返回。
-
基于 I/O 复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
-
Reactor 模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式。即 I/O 多了复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术
- Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。
- Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际办理人。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。
select
- select 其实就是把NIO中用户态要遍历的fd数组(我们的每一个socket链接,安装进ArrayList里面的那个)拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所有拷贝到内核态后,这样遍历判断的时候就不用一直用户态和内核态频繁切换了
- 相较于NIO,不需要在用户态遍历socket,而是放到数组中,拷贝到内核态进行
问题:
- bitmap最大1024位,一个进程最多只能处理1024个客户端
- &rset遍历指针不可重用,每次socket有数据就相应的位会被置位
- select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝浪费资源
- select并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历。
poll
epoll
- 只发送就绪状态的fd文件描述符
手写分布式锁
- DistributedLockFactory
@Component
public class DistributedLockFactory{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
public DistributedLockFactory(){
this.uuidValue = IdUtil.simpleUUID();//UUID
}
public Lock getDistributedLock(String lockType){
if(lockType == null) return null;
if(lockType.equalsIgnoreCase("REDIS")){
lockName = "zzyyRedisLock";
return new RedisDistributedLock(stringRedisTemplate,lockName,uuidValue);
} else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
//TODO zookeeper版本的分布式锁实现
return new ZookeeperDistributedLock();
} else if(lockType.equalsIgnoreCase("MYSQL")){
//TODO mysql版本的分布式锁实现
return null;
}
return null;
}
}
- RedisDistributedLock
public class RedisDistributedLock implements Lock {
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
private long expireTime;
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue){
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
this.expireTime = 30L;
}
@Override
public void lock(){
this.tryLock();
}
@Override
public boolean tryLock(){
try{
return this.tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if(time != -1L){
expireTime = unit.toSeconds(time);
}
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
}
this.renewExpire();
return true;
}
@Override
public void unlock()
{
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
"return redis.call('del',KEYS[1]) " +
"else return 0 " +
"end";
System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
if(flag == null) {
throw new RuntimeException("没有这个锁,HEXISTS查询无");
}
}
// 自动续期
private void renewExpire() {
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
// 计时器线程
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
renewExpire();
}
}
},(this.expireTime * 1000)/3);
}
//=========================================================
@Override
public void lockInterruptibly() throws InterruptedException {}
@Override
public Condition newCondition() { return null; }
}
- 业务类InventoryService
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale() {
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
//暂停几秒钟线程,为了测试自动续期
try { TimeUnit.SECONDS.sleep(120); } catch (InterruptedException e) { e.printStackTrace(); }
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
private void testReEnter() {
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
System.out.println("################测试可重入锁##############");
}finally {
redisLock.unlock();
}
}
}