超卖:乐观锁 mysql的,where 更新之前的数据=更新之后的数据。并发情况下:同一时刻错误性太高:有库存遗留问题。解决办法 where 要更新的数据>0
缓存穿透:指数据库和缓存里面都没的数据,解决办法:对空值""进行缓存。如:
查询缓存没得值,就去查数据库,没得值,我们久将查询的数据设置未空值,缓存进redis,并设置过期时间,下次查询,就会命中空值缓存,从而不是打进数据库。
缓存击穿:(互斥锁的方式)个别key失效的情况,互斥锁 setnx 串行化。一个请求释放锁后,另一个请求拿到锁。
如:查询商铺数据,一个线程查询key失效,就去获取锁,获取锁成功,就去查询数据库,并更新缓存,同1时刻,另外的线程拿不到锁,就重试(sleep ,在调用自己函数,递归)确定:并发性差,优点一致性高
缓存击穿:(逻辑过期的方式,(事先就有key,给这个key一个时间属性)在互斥锁的方式上优化,我们不阻塞线程,线程拿不到锁就返回旧数据,线程拿到锁,判断逻辑过期没有,过期了,就开个线程去查询数据库,并更新缓存,本身线程依旧返回旧数据,缺点:一致性比较差,存在查询到旧数据的可能性,优点:并发性好)
缓存的更新策略:先更新数据库,在删除缓存(查询的时候没有命中缓存,就回去查询数据库,然后再建立缓存)
Redisson 如何解决锁相关问题:
Redisson如何解决分布式锁主从一致性问题:
基操作:key只有一种类型 String
String: set key value | get key
底层实现是一个结构体里面有个char数组保存value的值
Hash: hset key key1 value1 key2 value2 | hget key key1
hash底层的结构是 ziplist 和 hashtable
那么,什么时候会从ziplist转成hashtable呢?这个在redis.conf中有相关的配置,如下:
默认情况下:
- 当ziplist中entry的数量超过512的时候,会转成hashtable ziplist底层是个双链表
- 单个元素的值超过64字节的时候,会转成hashtable
List:
应用场景: 针对有先后顺序的数据 :比如朋友圈点赞,评论 哪个先点 哪个后点是有顺序的
左侧是头 右测是尾 lpush 在左侧左边插入 不会再尾巴插入
事务:
用到的命令:multi exec discard
reids事务是否支持acid:
Atomicity:reids不具备原子性(一个事务的多个操作必须完成,或者都不完成)在reidis中事务分两个步骤完成 1、组队阶段 2、命令执行阶段
在组队阶段 set k3 没设置value 所以报错 ,然后在执行exec也报错,组队阶段具备原子性
下面例子在执行阶段报错,会导致只有错误的命令不会执行,没问题的命令依然会继续成功,所以redis不能保证原子性
一致性 redis可以保证
一致性会受到错误命令、实例故障发生时机的影响,按照命令出错实例故障两个维度的发生时机,可以分三种情况分析。
EXEC 执行前,入队报错
事务会被放弃执行,所以可以保证一致性。
EXEC 执行后,实际执行时报错
有错误的执行不会执行,正确的指令可以正常执行,一致性可以保证。
EXEC 执行时,实例故障
实例故障后会进行重启,这就和数据恢复的方式有关了,我们要根据实例是否开启了 RDB 或 AOF 来分情况讨论下。
如果我们没有开启 RDB 或 AOF,那么,实例故障重启后,数据都没有了,数据库是一致的。
如果我们使用了 RDB 快照,因为 RDB 快照不会在事务执行时执行。
所以,事务命令操作的结果不会被保存到 RDB 快照中,使用 RDB 快照进行恢复时,数据库里的数据也是一致的。
如果我们使用了 AOF 日志,而事务操作还没有被记录到 AOF 日志时,实例就发生了故障,那么,使用 AOF 日志恢复的数据库数据是一致的。
如果只有部分操作被记录到了 AOF 日志,我们可以使用 redis-check-aof 清除事务中已经完成的操作,数据库恢复后也是一致的。
隔离性
事务执行又可以分成命令入队(EXEC 命令执行前)和命令实际执行(EXEC 命令执行后)两个阶段。
所以在并发执行的时候我们针对这两个阶段分两种情况分析:
-
并发操作在
EXEC
命令前执行,隔离性需要通过WATCH
机制保证; -
并发操作在
EXEC
命令之后,隔离性可以保证。
持久性: Redis 本身是内存数据库,持久性并不是一个必须的属性,我们更加关注的还是原子性、一致性和隔离性这三个属性。
为什么要用到事务:
举个例子说说事务冲突
有三个人都用你的账号去购物 现在你的账号余额有10000 ,其中一个人准备买8000的东西,一个准备买5000的东西,一个人准备买2000,第一个人手快一点先去买了8000的商品,同时第二个人在去买,然后第三个买,假设同意时刻他们都进入到if分支内部,程序上已经校验过金额
最后会导致数据库内部的金额数据为负数,也就是常说的超卖问题,库存,金额不能为负。
为避免这种情况的产生需要使用事务 并进行加锁(乐观锁)解决但会导致库存遗留问题或者采用分布式锁解决或者采用lua机制进行保证
Redis的两种锁:
1、悲观锁 :
2、乐观锁:
先去操作的人,更新了数据后,也会同时更新版本号,其他人在更新数据的时候会先对比版本号是否一致,不一致则不能操作。乐观锁采用watch实现
终端1 2 同时监视balance 然后开启事务 执行incrby 然后分别执行exec 结果如下:
秒杀案列的实现以及产生的各种问题
static boolean doSecKill(String uid,String prodid) throws IOException{
1. 判断 uid及proid是否为空
2.连接redis
3.拼接key 1 库存key 2秒杀成功用户key
4.获取库存,如果库存null 秒杀还没开始
5 判断用户是否重复秒杀操作
6 判断商品数量,库存数量小于 1 ,秒杀结束
7 秒杀过程 商品-1 秒杀成功用户+1
8.关闭redsi连接
}
下面用 ab 工具进行并发模拟 会出现商品数量出现负数的情况:这种情况叫做超卖。
出现原因:
在并发情况下,假设某一时刻 redis种实际商品数量大于0,而此时多个线程或者进程代码执行已经到步骤6 且校验通过,在执行步骤7的时候都会成功,所以会导致超卖的问题,在多线程和多进程的情况下,这种并发并非安全。
解决措施:(乐观锁)事务+wtach +连接池(解决超时问题)
监视一下库存数量 multi 将步骤7采用事务进行处理 exc
但上述操作依然会存在问题:在并发场景下会产生库存遗留问题 ,在并发场景下,多个进程或者线程处于redis事务组队阶段,在最先执行的进程过线程在执行exec过后,会更新版本,其他线程或者进程执行exec 不会成功。(假设1000张票 2000个人抢票 ,在某一并发时刻,2000人种有1500个人处于并发事务中,其中1500个人中有一个人先更新数据 其余1499个人都更新不了数据,就会导致 票没被抢完的情况。
解决措施: redis默认没有悲观锁 将步骤7中的秒杀过程写进lua 进行保证。
分布式锁 setnx (上锁)+ expire(设置过期时间) del 释放锁
三联问:
1.使用setnx 上锁,通过del释放锁
2.自己忘记del释放锁或者服务器突然出现问题导致key没释放,可以设置key的过期时间
3.原子操作:上锁后,服务突然断电,过期时间无法生效,一直无法释放锁
解决措施 上锁的的时候同时设置过期时间 set key val nx ex 10 保证原子操作,但是又会出现4的问题(key过期时间设置的过小或者程序出现卡顿,导致程序还没执行完key就过期了,第二个人就可以拿到所,然后第二个人执行程序的时候,第一个人程序卡顿恢复了,最后调用del手动释放锁,把第二个的锁释放了,相当于没上锁)所以用uuid 自己的锁只能自己释放!
4.UUID 防止误删
1、使用uuid 例:
set lock uuid nx ex 10 释放锁的时候,首先判断get当前的uuid和设置是否一样,一样则释放
但是这个也有一个问题,释放的时候我们要先get key的值,get和释放又不是一个原子操作:a在判断 当前的uuid和设置的一样后 且在删除之前(出现卡顿) 过期时间刚好到了自动释放,B就可以获取到这个锁,然后B在执行的时候,锁又被A释放了。
解决措施:将 删除操作(get key 判断 和del key)这两个操作写进lua里面,调用lua保证原子性。
总结分布式锁的用法:
1.setnx lock uuid nx ex|px
2.执行具体操作
3:将 删除操作(get key 判断 和del key)这两个操作写进lua里面,调用lua保证原子性。
什么是缓存穿透
也就是说外部访问一个不存在的数据,缓存没有 这样每次去查数据库,导致数据库压力大
什么是缓存击穿
数据源是存在的,只是由于访问的个别Key过期正好当时有大量访问该key的操作,导致去查数据库,导致数据库压力增大甚至崩溃
1.使用互斥锁 左边的,缺点:同一时刻可能存在多个请求的堆积,优点:一致性高。2、使用逻辑过期的方式,优点:并发性高,但是一致性差一些,返回的可能是旧数据。
什么是缓存雪崩
在极少的时间段,查询大量的key集中过期的情况
Redis的持久化操作:
1、RDB(redis databasde)
RDB备份的执行过程:
RDB方式配置相关
默认名字
生成dump.rdb的路径
硬盘满了 关闭redis的写操作
持久化策略 多长时间至少多少个key变化 就写一次rdb 不建议使用:save是阻塞的
rdb的优点
1、适合大规模的数据恢复
2、对数据完整性和一致性要求不高更适合使用
3、节省磁盘空间
4、恢复速度快
rdb的缺点
1、Fork的时候,内存中的数据被克隆了一份,大致两倍的膨胀性需要考虑
2、写实拷贝技术,数据量大的时候还是比较消耗性能
3、备份周期在一定间隔做一次持久化,所以Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
AOF(append only file)
如果AOF和RDB同时开始,reids默认选取AOF的数据(数据不会丢失)
AOF 默认不开启
AOF同步频率设置 always始终同步 everysec 每秒同步 no 交给操作系统决定什么时候同步
AOF持久化流程:
AOF重写策略
AOF的优点
备份机制更稳健,丢失概率更低
可读日志文本,通过操作AOF稳健,可以处理误操作
AOF的缺点
比起RDB占用更多的磁盘空间
恢复备份速度更慢
每次读写都同步的话,有一定的压力
存在个别bug,造成不能恢复
Redis 集群
1.主从:
启动多个redis服务 选择从机执行slaveof ip(主机)+port(主机)
用info replication 查看主从是否设定成功
主机可读可写 从机只能读
优点:读写分离
缺点:主机挂了 就真挂了 无法做到高可用
薪火相传:一个slave可以事另外一台slave的master
反客为主:master挂了 slave里面执行slaveof no one 可以将slave升为master 缺点:手动执行
2.哨兵模式:
在master挂掉了 可以通过一系列算法将salve自动选举为master,后面master重启后 变为slave。
配置:
增加配置文件
1、取名 sentinel.conf 里面增加一行 sentinel monitor mymaster 127.0.0.1 1 其中mymaster为监控服务对象起的服务名 1至少有多少个同意迁移的数量
2、启动 redis-sentinel sentinel.conf 默认端口26379
选举规则 1、选择优先级靠前的 2、选择偏移量最大的 3、选择runid最小的从服务
3.无中心化集群:
解决什么问题: 1、容量不够如何进行扩容 2、并发写操作 如何分摊
配置:
1、基于主从基础配置文件中 每个redis服务的配置中增加下面三个配置
2.启动各个redis服务
3.将多个节点合成一个集群:到redis/src目录下执行 redis-cli --cluster create --cluster-replicas 1 192.168.11.101:6379 192.168.11.101:63780 192.168.11.101:6381 192.168.11.101:6382 192.168.11.101:6383 192.168.11.101:6384
--cluster-replicas 1 采用最简单的方式搭建集群 一台主机 一台从机
客户端采用 redis-cli -c -p 进行登录
采用cluster nodes 查看集群信息
一个集群至少要求三个主节点
根据 cluster-require-full-coverage 决定单一主从全挂掉 是否整个集群不可用
c++ 连接redis集群: 利用hredis进行连接 1、先随意连接一台redis服务,2、写入的时候根据redis返回数据 截取ip 端口 在进行连接执行写入命令,获取也是一样的。具体代码如下
Redis 存储原理:
1、对用户输入的key 进行hash(siphash函数) 得到一个64位的hash整形值
2、在对这个hash值对4(数组的大小) 取余,得到应该放在哪个数组下标下面, 用链表串联起来 采用头部插入法。
注:下标是经过key hash函数得到hash值在对数组大小取余得到的。
redis里面hash冲突的解决办法 数组加链表,同一个数组索引下面的不同k v 按照链表头插法进行插入。
1、分治的思想:每次操作,增删改查 set get del rehash一次,一次是数组的槽位(链表)
2、如果没有持久化操作,在1ms内执行多个100次操作这里的一次也是数组的一个槽位数据
Reids 主从复制原理
一、2.8 版本以前的方案:全量复制
三种情况: 1、新节点加入 2、主从连接故障 3、从节点重启
以上三种情况在2.8 版本都是全量复制:主要分为以上三步骤进行全量复制
1、master完成持久化写rdb 之后的命令写入发送缓冲区,发送rdb
2、slave接受rdb,加载至内存
3、master 发送缓冲区命令 slave一次处理
问题:主从连接故障时间短 完全没有必要全量更新,只需将少量的写操作更新。
二、2.8 版本增量同步
1、slave记录偏移量 psync 主id offset 发送给master master判断offset是否在环形缓冲区内。
增量同步:
Redis 常见面试问题
1、redis是单线程还是多线程?
2、Redis单线程为什么还能这么快?
3、Redis key过期了 为什么内存没有释放?
1、set key ex 后 在对这个key设置,没有加上过期参数
4、Redis key没设置过期时间为什么被redis主动删除了
有可能配置了针对所有key进行删除的策略b)默认第8个
5、Redis淘汰的算法LRU和LFU区别
6、删除key的命令del key会阻塞redis吗
会阻塞
时间复杂度 删除单个字符串类型的key 时间复杂度为0(1).
删除单个列表、集合、有序集合或哈希表类型的key,时间复杂度o(M),M为以上数据结构内的元素数量。
7、Redis主从切换导致的缓存雪崩是怎么造成的?
8、Redis持久化RDB AOF 混合持久化是怎么回事?
8、
9、
10、Redis线上的数据如何备份?
11、Redis集群网络抖动导致频繁主从切换怎么处理
12、Redis集群为什么至少需要3个master节点
13、
缓存雪崩解决办法:
1、将多个key的过期时间设置为不同,这样保证同一时间不会有多个key同时失效
2、程序刚启动的时候 redis里面是没有数据的 需要缓存预热 先将mysql的数据同步到redis里面
3、枷锁
14、如何解决缓存击穿问题?
1、个别热点key失效,并发过高导致的缓存击穿,导致数据库瞬间的压力增大,考虑key延期或者永不过期哦。
2、加互斥锁。
15、如何解决缓存穿透问题?
1、用户鉴权 id校验 id<0直接拦截(业务方便隔离部分)
2、对空值缓存(大量不存的id 导致reids内存增加)设置过期时间解决+读延期
3、采用布隆过滤器
16、如何保证数据库与缓存的一致性
1、先更新数据库在更新redis 缓存可能更新失败,读到老数据
2、先删除缓存在更新数据库,并发时,数据库没更新完成,读操作会读到老数据设回缓存
3、先更新数据 ,再删除缓存。可能存在删除失败的情况,(删除失败重试保证)3比 1 2 好
4、
5、分布式队列 先删除key 将更新数据库操作放进queue里面,再将读reids失败的操作 也一依次加进队列里面,串行执行,保证更新操作完成后再执行redis读操作。能保证数据的一致性:
缺点:
综上:选择方案三 先更新数据再删除缓存,读的时候没有查数据库,读完写入redis