1、通用命令
help exists
keys a*
keys *
del k1 k2 k3
expire a1
ttl a1
2、Redis的key允许有多个单词形成层级结构
多个单词之间用':'隔开,格式如下:
项目名:业务名:类型:id
3、Redis默认有16个仓库,编号从0至15
4、基本类型五种
5、jedis实例是线程不安全的,多线程环境下需要基于连接池来使用。
6、lettuce 是 线程安全的,对集群模式支持很好
7、jedis使用基本步骤
1)引入依赖 2)创建jedis对象,建立连接
3).使用Jedis,方法名与Redis命令一致
4).释放资源
8、SpringDataRedis的使用步骤:
1).引入spring-boot-starter-data-redis依赖
2).在application.yml配置Redis信息
3).注入RedisTemplate
9、RedisTemplate可以接收任意Object作为值写入Redis:
只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化(ObjectOutputStream字节形式)。
所以一般使用StringRedisTemplate 并手动序列化
11.RedisTemplate的两种序列化实践方案:
方案一:
1.自定义RedisTemplate
2.修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
方案二:
1.使用StringRedisTemplate
2.写入Redis时,手动把对象序列化为JSON
3.读取Redis时,手动把读取到的JSON反序列化为对象
18、缓存更新策略 一般有三种:内存淘汰 超时剔除 主动更新
19、缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
- 缓存空对象
- 布隆过滤
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
20、缓存雪崩
是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
21、缓存击穿问题
也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
22、逻辑过期
redis字段里额外存一个过期时间字段
23、缓存击穿问题解决
- 互斥锁
- 逻辑过期
其实都是在解决缓存重建这一段时间发生的问题
24、后台系统一般可以用来导入热点数据
25、如何确保数据库与缓存操作的原子性?
单体系统:利用事务机制
分布式系统:利用分布式事务机制
26、使用数据库自增ID就存在一些问题:
- id的规律性太明显
- 受单表数据量的限制 (分库分表时会出现重复id的情况)
27、全局唯一ID生成策略:
•(1)UUID
•(2)Redis自增
•(3)snowflake算法 (采用的是当前机器的自增,需要维护一个机器id,对时钟依赖较高)
•(4)数据库自增(用一个单独的表存自增字段)
Redis自增ID策略:
•每天一个key,方便统计订单量
•ID构造是 时间戳 + 计数器
Redis实现全局唯一Id timestamp COUNT_BITS | count;
33、优惠券秒杀涉及两张表
tb_voucher:存优惠券(普通券+秒杀券)信息 tb_seckill_voucher:存秒杀券的扩展信息
tb_voucher_order:订单记录信息
34、秒杀下单流程
35、
乐观锁
不加锁,更新时判断其他线程是否在修改
1)版本号法 库存作为版本号
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1") //set stock = stock -1
.eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ?
这个方式要修改前后都保持一致,但是这样我们分析过,成功的概率太低,所以我们的乐观锁需要变一下,改成stock大于0 即可
v2版本:
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0
优点:性能好
缺点:存在成功率低的问题
悲观锁
1)添加同步锁,让线程串行执行
优点:简单粗暴
缺点:性能一般
36、乐观锁比较适合更新数据,而现在是插入数据,所以我们需要使用悲观锁操作
37、一人一单出现并发问题分析过程
1)、初始方案是封装了一个createVoucherOrder方法,同时为了确保他线程安全,在方法上添加了一把synchronized 锁
分析:添加锁,锁的粒度太粗了,在使用锁过程中,控制锁粒度 是一个非常重要的事情,因为如果锁的粒度太大,会导致每个线程进来都会锁住,所以我们需要去控制锁的粒度
2)、第二步控制粒度
intern() 这个方法是从常量池中拿到数据,如果我们直接使用userId.toString() 他拿到的对象实际上是不同的对象,new出来的对象,我们使用锁必须保证锁必须是同一把,所以我们需要使用intern()方法
分析:还是存在问题,问题的原因在于当前方法被spring的事务控制,如果你在方法内部加锁,可能会导致当前方法事务还没有提交,但是锁已经释放也会导致问题,所以我们选择将当前方法整体包裹起来,确保事务不会出现问题
3)、第三步保证事务的特性
分析:但是以上做法依然有问题,因为你调用的方法,其实是this.的方式调用的,事务想要生效,还得利用代理来生效,所以这个地方,我们需要获得原始的事务对象, 来操作事务
4)、第四步使用代理
并需要使用AopContext获取当前类的代理类:
或者当前类成员变量中 注入自己
分析:但是以上做法依然有问题,集群下仍然会导致问题
5)、第五步 请移步到43号看分布式解决方案
38、spirng事务失效的一种可能现象:
没有使用代理对象,没有被spring去管理,,比如下面的调用:
方案 这时需要使用AopContext获取当前类的代理类:
或者当前类成员变量中 注入自己
39、使用AopContext 注意点
需要在启动类 添加注解:
@EnableAspectJAutoProxy(exposeProxy = true) // 默认暴露代理对象
xml添加依赖:
dependency>
groupId>org.aspectjgroupId>
artifactId>aspectjweaverartifactId>
dependency>
40、集群下锁失效原因分析
锁监视器
不同的服务器,jvm不同, 锁对象也不是同一个
41、分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
42、redis实现分布式锁的核心思路
核心思路:
我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可
43、
1)、针对分布式系统问题,利用redis实现分布式锁 版本1
利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性
* 5-利用redis分布式锁 版本1 */ Long userId = UserHolder.getUser().getId(); //创建锁对象(新增代码) SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate); //获取锁对象 boolean isLock = lock.tryLock(1200); // 判断是否获取锁成功 //加锁失败 if (!isLock) { return Result.fail("不允许重复下单"); } try { //获取代理对象(事务) IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder3(voucherId); } finally { //释放锁 lock.unlock(); }
分析:但是仍然存在问题,误删别人的锁
线程1拿到锁后业务阻塞,释放锁了;
线程2拿到锁后,执行逻辑,误删了锁。
解决方案:
在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除
2)、针对误删了锁问题,利用redis实现分布式锁 版本2
核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
分析:但是极端情况下还是会出现误删问题,删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的
解决方案:
Lua脚本解决多条命令原子性问题 ,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
3)、针对删锁时的原子性问题,利用redis实现分布式锁 版本3
实现拿锁比锁删锁是一个原子性动作
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
44、Lua脚本
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:Lua 教程 | 菜鸟教程
45、redis的事务可以保持原子性但是不能保证一致性
46、基于setnx实现的分布式锁存在下面的问题:
只能说setnx+lua就是一种经典方式实现分布式锁。
但企业中一般是引入redission。
47、什么是Redission呢
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
Redission提供了分布式锁的多种多样的功能
48、Redission使用步骤
1)引入依赖
2)配置Redisson客户端
3)引入客户端方法
49、Redission可重入锁原理
在redission中,我们的也支持支持可重入锁
在分布式锁中,他采用hash结构(hset)用来存储锁,其中大key表示表示这把锁是否存在,用小key表示当前这把锁被哪个线程持有,小value表示重入次数,所以接下来我们一起分析一下当前的这个lua表达式
获取锁的lua脚本 和 释放锁的lua脚本
两个方法lock 和 unlock源码重点位置:
其实就是lua脚本。
如果使用了带参的trylock方法,利用了消息队列和信号量的知识,看门狗默认时间,
currenthashmap:
EXPIRATION_RENEWAL_MAP (会存每一个线程对应的一个执行更新有效时间的定时任务)
原理总流程图:
需要复习请看67集
50、Redisson分布式锁原理:
•可重入:利用hash结构记录线程id和重入次数
•可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
•超时续约:利用watchDog,每隔一段时间(releaseTime / 3),重置超时时间
•主从一致性:利用MutiLock 加锁 (联锁)
51、分布式锁 三种方式总结
1)不可重入Redis分布式锁:
u原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
u缺陷:不可重入、无法重试、锁超时失效
2)可重入的Redis分布式锁:
u原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
u缺陷:redis宕机引起锁失效问题
3)Redisson的multiLock:
u原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
缺陷:运维成本高、实现复杂
55、使用队列的好处在于 解耦
56、Redis提供了三种不同的方式来实现消息队列:
list结构:基于List结构模拟消息队列
PubSub:基本的点对点消息模型
Stream:比较完善的消息队列模型
57、 Redis消息队列-基于Stream的消息队列
Stream 是 Redis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。
https://redis.io/commands/?group=stream
58、基于stream的单消费模式
- 有消息漏读的风险
59、基于stream的消费者组模式
解决 消息漏读的风险
消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:
60、监听循环的思路:
61、spring注解使用
//在类初始化之后执行,因为当这个类初始化好了之后,随时都是有可能要执行的
@PostConstruct
private void init() {
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
62、
63、点赞实现步骤
使用set 数据结构
实现步骤:
- 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
- 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
- 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
64、点赞排行榜
使用sortedSet 数据结构
由于多了排行榜的需求,所以需要修改之前的逻辑。
注意保证点赞的顺序:
ORDER BY FIELD(id," + idStr + ")
65、关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示
66、
共同关注 有交集并集补集的api set集合中的交集数据
选用数据结构 set
set交集 intersect
67、Feed流产品有两种常见模式
Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
68、
采用Timeline的模式。该模式的实现方案有三种:
- 拉模式
- 推模式
- 推拉结合
69、好友关注-推送到粉丝收件箱
核心的意思:就是我们在保存完探店笔记后,获得到当前笔记的粉丝,然后把数据推送到粉丝的redis中去。
选用数据结构 ZSet 方便后期按照时间顺序获取
70、滚动分页:
我们需要记录每次操作的最后一条,然后从这个位置开始去读取数据
71、附近商铺
Geo 经纬度
Geo 底层也是 zset
redis存储 店铺I'd+经纬度坐标
分组 stream groupingby
Maven helper 插件
31bit bitmap
4个字节
Hutool
72、Pv. Uv
Hyperloglog
73、
单机的Redis存在四大问题:
数据丢失问题
并发能力问题
存储能力问题
故障恢复问题
74、Redis有两种持久化方案:
- RDB持久化
- AOF持久化
Redis默认开启RDB
AOF默认关闭的,主要记录日志操作
75、RDB原理
RDB方式bgsave的基本流程?
- fork主进程得到一个子进程,共享内存空间
- 子进程读取内存数据并写入新的RDB文件
- 用新RDB文件替换旧的RDB文件
RDB会在什么时候执行?save 60 1000代表什么含义?
- 默认是服务停止时
- 代表60秒内至少执行1000次修改则触发RDB
RDB的缺点?
- RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
- fork子进程、压缩、写出RDB文件都比较耗时
76、 AOF三种策略对比:
项目一般使用everysec
77、禁用RDB
配置文件中配置: save ""
78、AOF文件执行重写功能
通过执行bgrewriteaof命令,可以 减少文件体积,异步执行的
79、RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。
rdb 备份
aof 日志
80、
主从模式 需要把 rdb开启, aof关闭
81、查看主从状态命令
info replication
82、
假设有A、B两个Redis实例,如何让B作为A的slave节点?
•在B节点执行命令:slaveof A的IP A的port
83、主从同步时master如何得知salve是第一次来连接呢??
有几个概念,可以作为判断依据:
- Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
- offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
因此slave做数据同步,必须向master声明自己的replication id 和offset,master才可以判断到底需要同步哪些数据。
84、主从全量同步完整流程描述:
- slave节点请求增量同步
- master节点判断replid,发现不一致,拒绝增量同步
- master将完整内存数据生成RDB,发送RDB到slave
- slave清空本地数据,加载master的RDB
- master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
- slave执行接收到的命令,保持与master之间的同步
85、
主从第一次同步是全量同步,但如果slave重启后同步,则执行增量同步
86、增量同步注意点
87、主从同步优化
主从同步可以保证主从数据的一致性,非常重要。
可以从以下几个方面来优化Redis主从就集群:
- 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。
- Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
- 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
- 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
主从从架构图:
88、主从同步小结
简述全量同步和增量同步区别?
- 全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。
- 增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
什么时候执行全量同步?
- slave节点第一次连接master节点时
- slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
什么时候执行增量同步?
- slave节点断开又恢复,并且在repl_baklog中能找到offset时
89、slave节点宕机恢复后可以找master节点同步数据,那master节点宕机怎么办?
哨兵模式
90、Sentinel充当Redis客户端的服务发现来源
91、Sentinel原理
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
•主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
•客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
92、选举机制
一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
- 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
- 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
- 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高 (这一步比较关键)
- 最后是判断slave节点的运行id大小,越小优先级越高。
93、Sentinel小结
Sentinel的三个作用是什么?
- 监控
- 故障转移
- 通知
Sentinel如何判断一个redis实例是否健康?
- 每隔1秒发送一次ping命令,如果超过一定时间没有相向则认为是主观下线
- 如果大多数sentinel都认为实例主观下线,则判定服务下线
故障转移步骤有哪些?
- 首先选定一个slave作为新的master,执行slaveof no one
- 然后让所有节点都执行slaveof 新master
- 修改故障节点配置,添加slaveof 新master
94、RedisTemplate集成哨兵机制
在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户[[端必须感知这种变化,及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。
95、分片集群应对海量数据
如图:
分片集群特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
96、分片集群小结
Redis如何判断某个key应该在哪个实例?
- 将16384个插槽分配到不同的实例
- 根据key的有效部分计算哈希值,对16384取余
- 余数作为插槽,寻找插槽所在实例即可
如何将同一类数据固定的保存在同一个Redis实例?
- 这一类数据使用相同的有效部分,例如key都以{typeId}为前缀
97、数据跟着插槽走,就能保证数据不丢失,因为插槽是可以转移的
98、分片集群转移插槽命令
reshard
100、两种
主从+哨兵
分片集群