1.关于jvm的synchronized关键字锁,只是针对当前运行的这台虚拟机有效果,当使用分布式架构nginx来部署系统时,项目部署在两个及以上的服务器上,使用nginx来管理对外的链接请求,此时再使用synchronized来控制原子性就会有问题;对于一个需要操作的对象,我们可以创建redis的分布式锁来处理。Redisson加锁代码,分布式锁操作redis,由于redis是单线程的,所以当有线程锁住某个对象的锁时,其它操作此对象的线程只能自旋等待,此处可以使用分段锁进行优化锁(拆分段、合并操作库存)。
String lockKey = "product_001";
RLock redissonKey = redisson.getLock(lockKey);
try {
redissonKey.lock(); //加锁
//处理业务代码,例处理库存
} finally {
redissonKey.unlock(); //释放锁
}
2.缓存与数据库双写不一致问题:线程一往数据库中写入一个变量值,然后出现卡顿,线程二也往此变量中写入另外一个值,然后再把此值写入到缓存中,之后线程一才把它的值覆盖写入到缓存中,这样此时数据库中存的是线程二赋的值,而缓存中存的是线程一赋的值,导致数据库与缓存不一致
解决方案:可以使用分布式锁,是多线程的处理逻辑串行化,这样可以解决问题,但是性能偏低
3.redis底层存储基于hash表,使用数组加链表进行存储;对key进行取模运算,放到对应的位置。
4.redis的value类型:字符串string、哈希hash、列表list、集合set、有序集合zset;string运用:单个缓存、对象缓存(json来回转)、分布式锁;hash运用:对象缓存;对象缓存中:对象的单个属性修改比较多推荐使用hash缓存,整个对象一块修改的情况多推荐使用string缓存。list运用:stack(栈):LPUSH+LPOP;queue(队列):LPSHT+RPOP;阻塞队列:LPUSH+BRPOP(有元素直接获取,没有元素等待有元素了获取),可以用好list方式存放消息动态数据,每 产生一条记录,往list 中插入一条记录,这样从list的左边开始,数据是一个降序的排序方式,LRANGE ID 0,4:查看最新的5条数据。
set运用场景:sadd key {value}(添加一个数据到集合)、smembers key(查看某个set下的元素集合)、srandmembers key 【count】(从set集合中随机抽取几个数据)、spop key 【count】(从set集合中随机取出几个数据,并且删除它)
集合操作
set1{a,b,c} set2{b,c,d} set3{c,d,e}
sinter set1 set2 set3 {c]:求交集
SUNION set1 set2 set3 {a,b,c,d,e}:求并集
SDIFF set1 set2 set3 {a}:求差集,第一个集合减去后面集合的并集,然后以第一个集合为准,找出第一个集合没有出现在在后面集合中的元素
Zset运用场景:展示当日排名前十:ZREVRANGE hostNew:20200120 0 9 withscores
5.微博微信的关注模型实现方式
(1)查询共同关注的人:Sinter set1 set2
(2)我可能认识的人:sdiff set1 set2
6.微博微信点赞收藏功能
(1)点赞:sadd like:消息id {用户id}
(2)取消点赞:srem like:消息id {用户id}
(3)检查用户是否点过赞:sismembers like:消息id {用户id}
(4)获取点赞的用户列表:smembers key
7.redis存储结构
(1)简单动态字符串
(2)哈希表
(3)压缩列表(ziplist)
(4)双向链表
(5)整数数组
(6)跳表(skiplist有序集合Zset可以使用跳表和压缩列表):有序链表每两个数据提取一个数字作为索引层,将有序链表改造成支持“折半查找”算法,可以进行快速的插入、删除、查询操作
8.简单命令
flushdb:清空表
type xx:xx对象的存储类型(5种)
object encoding xx:xx的底层存储结构(6种)
9.zset的存储结构有压缩链表(ziplist)变为跳表(skiplist)的条件,系统redis.conf中默认配置
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
元素个数超过128个,由压缩链表变为跳表方式;单个元素大小超过64byte,使用跳表存储
10.数据持久化的操作RDB、AOP机制
(1)RDB:在指定的时间间隔内把内存中的数据集快照写入磁盘,实际操作是fork一个子进程,先将数据集写入零时文件,写入成功后再替换之前的文件,用二进制压缩存储。
(2)AOF:以日志的形式详细记录每一个redis的写、删除操作,查询操作不会记录,以文本的方式记录
11.redis过期键删除策略
(1)惰性过期:只有当访问一个key时,才判断此key是否过期,过期则删除。可以最大化的节省cpu资源,对内存不友好
(2)定期过期:每个一段时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除已经过期的key
12.redis单线程快的原因:
(1)纯内存操作
(2)核心是基于非阻塞的IO多路复用机制
(3)单线程避免了多线程的频繁上下文切换带来的性能问题
13.缓存雪崩、缓存穿透、缓存击穿
(1)缓存雪崩:缓存同一时间大面积的失效,后面的请求都会落在数据库上,造成数据库短时间内承受大量请求而崩掉
解决方案:
①缓存数据的过期时间设置随机,防止同一时间大量数据过期过期现象发生
②给每个缓存数据增加缓存标识,记录缓存是否失效,如果缓存标记失效,则更新数据缓存
③缓存预热
④互斥锁
(2)缓存穿透:缓存和数据库中都没有的数据,导致所有的请求都落在数据库上,造成数据库短时间内承受大量请求而崩掉
解决方案:
①接口层增加校验,如用户鉴权校验,id做基础校验,id《=0的直接拦截
②从缓存中取不到的数,从数据库中也没有取到,这时也可以将key_value对写成key-null,缓存有效时间可以设置端点,设置太长会导致正常情况也没法使用
③采用布隆过滤器
(3)缓存击穿:缓存中没有,数据库中有,一般发生在缓存时间到期,这时由于并发用户特别多,同时读缓存没有读到数据,又同时去数据库中查询,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不一样的是,缓存击穿指并发查询同一条记录,缓存雪崩是不同数据都过期了,很多数据都查不到从而从数据库查
解决方案:
①设置热点数据永远不过期
②加互斥锁
14.redis事务实现
(1)事务开始:MULTI命令的执行,标识着一个事务的开始,MULTI命令会将客户端状态的flags属性中打开REDIS_MULTI标识来完成
(2)命令入队:如果客户端发送的命令为multi、exec、watch、discard中的任何一个,服务器立即执行这个命令,若是其它命令,检查命令的正确性,命令不对的话直接返回错误信息给客户端,命令正确,放入事务队列中
(3)执行事务
redis不支持事务回滚机制,但是会检查事务中的命令是否正确
15.redis集群方式
(1)主从(哨兵模式):sentinel哨兵主要功能:
集群监控:监控redis master和slave进程是否正常工作
消息通知:若果某个redis实例有故障,哨兵负责发送消息作为报警通知给管理员
故障转移:如果master node挂掉了,会自动转移到slave node节点上
配置中心:如果故障转移发生了,通知client客户端新的master地址
故障转移时,判断一个master node是否宕机了,需要大部门的哨兵都同意才行,涉及到分布式选举
即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的
哨兵通常需要3个实例来保证自己的健壮性
(2)redis cluster:是服务端sharding技术,3.0版本开始正式提供,采用slot(槽),一共分为16384个槽
(3)redis sharding:是客户端分片,由客户端进行hash计算,把key放到哪个节点上
16.redis主从复制原理
(1)全量复制:主节点通过bgsave命令fork一个子进程进行RDB持久化,改过程是非常消耗cpu,内存、硬盘io;
主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会发生很大的消耗;
从节点清空老数据,载入新的rdb文件是阻塞的,无法响应客户端的命令
(2)部分复制:复制偏移量:执行复制的双方,主从节点,分别会维护一个复制偏移量offset;
复制积压缓冲区:主节点内部维护了一个固定长度、先进先出对列作为复制积压缓冲区;当主从节点的赋值offset过大超过缓冲区长度时,进行全量复制;
服务器运行id:每个redis都有其运行的id,运行id有节点在启动时自动生成,主节点会将自己的运行id发送给从节点,从节点会将主几点的运行id存起来。从节点断开重连的时候,就是根据运行id来判断同步的进度
17.Redisson实现分布式锁的原理
(1)加锁机制
现在某个客户端要加,如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。
这里注意,仅仅只是选择一台机器!这点很关键!
紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:
为啥要用lua脚本呢?
因为一大坨复杂的业务逻辑,可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性。
那么,这段lua脚本是什么意思呢?
KEYS[1]代表的是你加锁的那个key,比如说:
RLock lock = redisson.getLock("myLock");
这里你自己设置了加锁的那个锁key就是“myLock”。
ARGV[1]代表的就是锁key的默认生存时间,默认30秒。
ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:
8743c9c0-0795-4907-87fd-6c719a6b4586:1
给大家解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。
如何加锁呢?很简单,用下面的命令:
hset myLock
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:
上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。
接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。
好了,到此为止,ok,加锁完成了。
(2)锁互斥机制
那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?
很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。
接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。
所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。
此时客户端2会进入一个while循环,不停的尝试加锁。
(3)watch dog自动延期机制
客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?
简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。
(4)可重入加锁机制
那如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢?
比如下面这种代码:
这时我们来分析一下上面那段lua脚本。
第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。
第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”
此时就会执行可重入加锁的逻辑,他会用:
incrby myLock
8743c9c0-0795-4907-87fd-6c71a6b4586:1 1
通过这个命令,对客户端1的加锁次数,累加1。
此时myLock数据结构变为下面这样:
大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数
(5)释放锁机制
如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。
其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。
如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:
“del myLock”命令,从redis里删除这个key。
然后呢,另外的客户端2就可以尝试完成加锁了。
这就是所谓的分布式锁的开源Redisson框架的实现机制。
一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。
(6)上述Redis分布式锁的缺点
其实上面那种方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。
但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。
接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。
此时就会导致多个客户端对一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。
所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。
18.redis项目运用
(1)数据字典数据
(2)分布式系统加锁redisson
(3)rabbitmq避免重复消费时,abbitmq将消息交个其它消费者时,先执行setnx(如果key存在,什么事情都不做,如果key不存在,正常的set方法);若果key已经存在,获取到它的值,如果是0,当前消费者什么都不做,如果是1,其它消费者已经消费过此消息,直接ack
(4)存在幂等性问题的接口,使用redis生成token,当提交数据时,使用del key
正常情况下返回结果为删除键的个数,假如删除了一个不存在的键,就返回0;判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行
(5)点赞、收藏数据
19.如何保证redis高可用
(1)redis 主从架构
单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。
redis 的高可用架构,叫做 failover 故障转移,也可以叫做主备切换。
master node 在故障时,自动检测,并且将某个 slave node 自动切换为 master node的过程,叫做主备切换。这个过程,实现了 redis 的主从架构下的高可用。
(2)Redis 哨兵集群实现高可用
- 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
- 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
- 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
- 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。