参考文档:https://blog.csdn.net/oneby1314/article/details/113789412
redis是单线程数据库,在开多个客户端同时发送指令的情况下,服务端接受的指令会以队列的形式依次执行,不会出现同一时间执行两个指令。
1、缓存高频访问的数据,缓解数据库的压力
2、分布式锁,结合redisson
3、信号量,可以完成秒杀等业务场景
Redis的数据都是以key-value形式,我们所说的数据结构是value的结构,包括以下几种:
1、String
常用指令:get,set
2、hash:hash_table形式,key-value,key不允许重复,value可以重复可以为null
常用指令:hget,hset
3、list:双向链表,元素可以重复,且保证有序
常用指令:rpush,lpush,lrange
4、set:hash集合,是基于hashtable,使用hashtable的key存储数据,value为null,所以不允许元素重复,且无序
5、zset:有序集合,可以用来去重,获取排名前几的用户
Redis事务是一个单独的隔离操作,事务中所有命令都会序列化,按顺序的执行,事务在执行的过程中,不会被其他客户端发来的命令请求打断。所以Redis事务是在一个队列中,一次性、顺序性、排他性的执行一系列命令。
1、mulit命令:使用mulit开始事务,开启后,接下来输入的命令将存入一个队列,当执行exec时提交命令,执行discard时放弃执行。
2、watch命令:在执行mulit指令前,可以使用watch指令来监控一个或者多个key,当在事务未提交前,watch监控的key被修改了,则事务会提交失败
3、discard命令:取消事务,放弃执行队列内的所有命令,退出事务。同时放弃watch监视,相当于执行了unwatch命令。
事务执行的三个阶段:开始事务(MULTI)===》命令入列 ===》执行事务(EXEC)
Redis事务为什么不支持回滚:
(1)一是因为Redis追求高性能操作,所以设计上减少事务回滚这些复杂操作
(2)二是因为Redis在执行命令的时候只会因为语法错误和key的数据类型不匹配导致报错,但这都是在开发阶段可以检测出来的,不应该让其出现在生产上。
Redis有两种持久化机制,RDB和AOF。
rdb机制的操作是,redis在指定的时间更新策略下,将redis内存的数据以快照的形式保
存到文件中,恢复是指redis在重启的时候将快照读取到内存中。
具体操作:具体操作是redis会fork一个子进程(主进程会短暂阻塞),先将内存中的数据集写入到一个临时文件中,写入成功后,再替换掉之前的文件,是采用二进制压缩存储。
优点:
(1)相比AOF,RDB更适合大规模的数据恢复,比AOF的启动效率高。
(2)只有一个dump.rdb文件,方便持久化。
(3)fork出一个子进程后,之后再由子进程完成持久化工作,主进程可以正常工作,也是讲性能最大化了。
缺点:
(1)数据安全性低,因为它是在一定时间间隔内做一次备份,如果Redis突然宕机,会丢失最后一次快照的修改。
(2)由于RDB是fork一个子进程来完成持久化工作,fork子进程会拷贝主进程的内存,因此当主进程占用内存比较大时,fork操作会导致主进程阻塞的时间比较长。
AOF持久化机制是指redis每接收客户端的一条写、删指令,就会直接将指令记录到文件中,恢复的时候相当于将记录的指令全部重跑一遍。AOF在记录的时候可以设置为每执行一条指令都记录,但是这样比较浪费性能,一般设置为每秒执行多少条指令记录一次。
优点:
(1)AOF机制比RDB具有更高的安全性,AOF有每秒同步和每修改同步,数据丢失的概率更小。
(2)AOF采用的是追加的方式写入文件,因此在写入的过程中即使出现宕机的现象,也不会破坏已经写入的内容。
(3)RDB是采用的二进制形式格式存储,AOF是直接记录的指令,相比RDB更清晰易懂。
缺点:
(1)相比同数据集而言,AOF文件要比RDB文件更大。所以AOF数据恢复的时候也比RDB要慢。
1、定时过期:每个设置过期时间的key都需要创建一个定时器,到期定时删除。浪费CPU性能去做定时器。
2、惰性过期:只有当访问一个key时,才会判断key是否已过期,过期则清除。存在极端情况就是当大量过期的key没有再次被访问,从而不会被清除,依旧占用大量内存。
3、定期删除:每隔一段时间检查部分的key,删除过期的key。
reids同时采用了惰性过期和定时过期两种策略。
(1)volatile-lru:当Redis内存不足时,会从设置了过期时间的键中移除那些最少使用的键。(该策略是采用的LRU算法,使用双向链表和哈希作为数据结构)
(2)volatile-ttl:当Redis内存不足时,会从设置了过期时间的键中移除将要过期的。
(3)noeviction:当内存空间不足时,新的写入操作就会报错。
1、主从复制:Redis主从复制可以设置为“一主多从”或“级联结构”模式。
2、主从复制的作用:
(1)实现了数据的热备份,是持久化之外的一种备份方式。
(2)故障恢复:当主节点出现问题后,可以由从节点提供服务。
(3)读写分离,负载均衡:由主节点提供写服务,从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据应用时连接从节点),使主从节点的压力得到均衡,实现服务高可用。
3、主从复制的方式
(1)全量同步
(2)增量同步
Redis增量同步是指Slave初始化后开始正常工作时,主节点每执行一个写命令就会向
从节点进行同步。
注意:建议在主节点开启数据持久化
4、“一主两从三哨兵”模式
目前我们开发环境的机器有限,我在开发环境搭建了“一主两从三哨兵”,总共三台机器。
在分布式系统中,服务一般会部署多个服务,当多个服务同时更新一个资源时,如果没有分布式锁的保证,那么就会产生最后的结果和期望的不一致。
(1)互斥性:同一时刻只能有同一个线程拿到锁。
(2)独占锁:解铃还须系铃人,加锁和解锁最好由一个线程完成,不要一个线程加锁了,另外的线程可以解锁。
(3)支持锁超时:当某个线程持有锁过后,由于业务报错或系统宕机导致不能释放锁,要能支持锁到期自动释放;
(4)可重用性:同一个线程能够多次获取锁;
(5)高可用性:比如使用redis实现分布式锁,如果使用单机部署模式,那么会一台redis挂掉后导致业务系统无法使用。所以redis需要实现集群部署,保证高可用性。
(1)使用setnx命令(错误做法)
setnx命令是当key不存在时才设置,设置成功返回1,设置失败返回0;
存在问题1:
setnx可以保证锁的互斥性,但是不能保证不能最终锁一定释放,比如执行业务代码时报错了。有人说可以使用try-catch-finally,但是如果是业务系统执行到一半就宕机了呢,那这个时候也不能释放锁,所以最好设计一个锁超时自动释放机制。
(1)使用setnx+expire命令(错误做法)
setnx命令是当key不存在时才设置,设置成功返回1,设置失败返回0;expire是给key增加过期时间。使用这两个命令有问题的原因是因为setnx和expire是分开两步操作的,不具有原子性,如果执行完setnx后就宕机了,那么后面也就无法设置过期时间了。
(2)使用Lua脚本(包含setnx和expire两条指令)
Lua脚本的执行会保证原子性。
"if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
"redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end"
(3)使用set key value [EX seconds][PX milliseconds][NX][XX]命令
eg:set resource_name my_random_value NX PX 30000
存在问题2:
比如我设置超时时间为30秒,那么当我们的业务执行耗时超过了30秒,这个时候锁已经自动释放了,并且另外一个线程拿到了锁开始执行业务,这时就有两个线程同时在执行业务了。
在加锁时,设置了过期时间,然后再开一个守护线程,定时去检测这个锁的失效时间,如果锁要过期了,但是业务执行还未完成,那么就自动多锁进行续期,重新设置过期时间。
在2.2中存在线程1加锁超时且自动释放后,线程2拿到锁,这时候线程1执行完了业务逻辑,执行删除锁命令,那么这时删除的就是线程2的锁了。
解决方案:为每一个锁设置一个唯一标识的UUID,在删除锁的时候进行判断,保证每个线程删除只能删除自己的锁。
可以在value中设置一个计数器,每加一次锁,计数器就加1,释放锁就减1。
当线程1获取了锁,线程2怎么知道线程1什么时候释放锁呢?
解决方案:(1)可以使用while true的方式轮询,缺点是比较耗费资源,浪费性能
(2)通过Redis的发布订阅功能实现,当第一次获取锁失败后就订阅释放消息;释放锁时需要发送释放锁的消息。
使用Redis实现分布式锁,最好使用集群部署,保证高可用。
RedLock算法:
redisson实现:
https://www.51cto.com/article/682636.html
https://cloud.tencent.com/developer/article/2031784
在我们常说的缓存雪崩、穿透、击穿实际上最根本的原因都是因为缓存失效了。只是我们要理解失效的原因和解决方案。
缓存雪崩可以从雪崩二字理解,就是存在大量的key失效,导致访问缓存的线程都到了数据上,给数据库造成了巨大压力。
常见造成缓存雪崩的原因:
(1)Redis服务宕机
(2)同时一时刻存在大量的key过期
解决方案:
(1)Redis服务最好使用集群模式,保证高可用。
(2)检查业务逻辑,避免存在同一时刻给key加相同过期时间,可以在过期时间上加上随机数。
缓存击穿是指热点key的失效,当某个热点key失效后,大量的线程访问不到这个key,就会访问数据库,那么也造成数据库的巨大压力。
常见造成缓存击穿的原因:
(1)热点key的过期,比如秒杀中,当缓存中的商品过期。
解决方案:
(1)热点数据不设置过期时间,使用定时清除策略。
(2)加锁,只有获取到锁后才能去访问热点数据。
缓存穿透是指用户访问的key没有在缓存上,而穿透缓存直接来到了数据库。
常见造成缓存穿透的原因:
(1)key被误删了,或者过期了
(2)用户恶意访问不存在的key
解决方案:
(1)参数校验,校验用户的id,拦截不合法id的请求
(2)空值保护,当第一次访问数据后,发现key不存在,就在缓存上保存一个key-null的空值,避免下次该key又到了数据库。
(3)使用布隆过滤器,布隆过滤器是在访问缓存数据前,先使用布隆过滤器进行校验,校验通过才可以访问缓存。
布隆过滤器的操作流程:
(1)布隆过滤器的底层是使用的bit数组存储数据,初始值都是0
(2)布隆过滤器第一次初始化的时候,先将数据库中的key进行hash计算,得到在布隆过滤器中的位置,将此位置的值更新为1,代表该位置的key是数据库中存在的。为了防止哈希冲突,可以对key多次hash操作。
(3)进行前面的操作后,用户在访问缓存前都要经过布隆过滤器的校验,只有用户请求的key在布隆过滤器中的值为1时才允许访问缓存。
布隆过滤器存在的问题:
(1)当布隆过滤器上某个位置的值需要删除时,会引发一系列的key失效,因为在进行hash计算时存在多个key的hash值相同,那么在布隆过滤器上的位置也就相同。
(2)当更新数据时,当数据新插入到物理库后需要更新布隆过滤器,如果更新布隆过滤器失败,那么就导致无法访问该key。
强一致性:当更新线程执行成功后,保证后续线程一定获取的是更新后的数据。
弱一致性:当更新线程执行成功后,不一定保证后续线程获取的是更新后的数据。
最终一致性:当更新线程执行成功后,经过一段时间的处理才能最终保证后续线程获取的是更新后的数据。
更新缓存是一个坑,有两个问题:
1️⃣ 更新缓存失败了,那缓存中还是原来的数据,后续线程还是获取的旧数据。
2️⃣ 一般我们放在缓存中的数据都是经过一系列计算和处理后的数据,并非直接将数据库的数据放到缓存,所以更新缓存存在性能消耗的代价,如果更新操作比较频繁,那么消耗的性能比较大。
同上(1)
此类操作最大的问题是要保证删除缓存成功,一旦删除缓存失败,那么缓存中保留的还是旧数据。要保证删除缓存成功有两种机制:一是利用消息队列实现删除重试机制;二是订阅数据库日志进行删除
(1)消息队列删除重试机制:
(2)订阅数据库日志: