一.redis的应用场景
- 缓存
- 分布式锁
- Token存储
- 短信验证码存储
- 计数器
- 全局唯一id
- 排行榜
- 限流
- 购物车
- 点赞关注
- 分布式Session
- 发布订阅
- 延迟队列
- 消息队列
1.分布式锁
锁,即在多线程环境下,对共享资源的访问造成的线程安全问题,通过锁的机制来实现资源访问互斥;比如Java语言有线程锁:synchronize / Lock 等;
实现方式有: Jedis ; Lettuce ; RedisTemplete ; Redisson ; 命令:setnx、expire、del
命令的形式:
1、获取锁:setnx key value
2、设置锁过期时间:expire key 30
3、执行业务代码
4、释放锁:del key
Redisson实现:
1、获取锁:redisson.getLock("lock"); lock.lock();
2、执行业务代码
3、释放锁:lock.unlock();
注意:Redisson锁的续期就是开一个一个线程一直监控key的过期状态,每隔1/3的过期时间就重置一次key的时间,防止业务没有完成,造成锁被释放,有其他线程进来,造成一些库存超卖等一些问题
①Redisson实现的分布式锁是可重入的吗?
可重入:即可重复获取,它指的是 线程T 获取到 锁A 之后,线程T再次获取 锁A 还是可以获取到的,java中的synchronized、ReentrantLock都是可重入锁。所以是可重入的,在一些特定场景可重入的锁非常有必要,比如递归情况下获取锁,就必须是可重入的,否则线程会一直处于等待状态。
②实现分布式锁需要注意哪些问题(坑)?
Ⅰ.不是原子操作; 可以用lua脚本保证,他可以把命令一次执行完Ⅱ.没有释放锁; 给key设置过期时间,避免得不到释放
Ⅲ.释放了锁,但业务还未执行完; 进行续锁操作
Ⅳ.释放了别人的锁; 抢到锁是设置唯一值,比如当前线程号+UUID,释放的时候进行对比
Ⅴ.大量请求竞争锁失败; 重试等待之后再获取 让业务执行尽可能短 限流
Ⅵ.多节点Redis主从复制的问题; (红锁进行解决)redis已过期了,可以通过zookeeper
向所有的redis节点加锁,比如我们此处节点数为3。
如果N个节点中,有N/2 + 1个节点 加锁成功了,那么整个加锁成功。
如果N个节点中,小于N/2 + 1个节点加锁成功,那么整个加锁失败。
如果各节点加锁总耗时,大于等于设置的最大等待时间,则直接返回加锁失败
redisson保证AP可用性,zookeeper保证CP一致性 大部分情况只需要保证可用性AP
Ⅶ.锁的性能问题; 分段锁,将库存分为n份锁就有n份,根据请求id进行hash运算,取余数获取不同的锁
Ⅷ.锁的可重入性;
即可重复获取, 它指的是 线程T 获取到 锁A 之后, 线程T 再次获取 锁A 还是可以获取到的, java中的synchronized、ReentrantLock都是可重入锁。
2.缓存穿透
缓存穿透是由于 请求一个不存在的数据 而导致的;
方案一:缓存空结果,对数据库查询不存在的数据仍然缓存到缓存中,比如缓存一条空值 unknow,这样有效减少查询数据库的次数; 优点:实现比较简单; 缺点:缓存了无效数据,占用Redis内存,可能存在缓存和数据库不一致的情况;
方案二:布隆过滤器; 优点:不会缓存无效数据; 缺点:实现比较复杂,存在一定的误判;
1、向布隆过滤器中添加元素时,会使用多个哈希函数对元素进行哈希,然后用数组长度取余数,算出一个索引位置值,再把数组的这几个位置都设置为1,这就完成了元素的添加操作; 2、向布隆过滤器查询元素是否存在时,和添加元素一样,也会把元素通过多个哈希计算出数组的位置,然后看数组中对应的几个位置是否都为1,只要有一个位置为0,就表示布隆过滤器里不存在该元素。如果这几个位置都为1,则认为这个元素可能存在,注意这里是可能存在,并不是一定存在(原因下面解读)
作用: 布隆过滤器可以判断某个元素是否存在于自身结构中;
特点: 当布隆过滤器判定某个值存在时,其实这个值只是有可能存在; 当布隆过滤器判定某个值不存在时,那这个值肯定不存在;
即:布隆过滤器有一定的误判概率;
注意:redisson可以实现布隆过滤器
3.缓存击穿
高并发条件下,对于热点数据(一般地,80%的情况下都是访问某些热点数据,也就是访问某些热点key,其他key访问会比较少),当数据失效的一瞬间,或者刚开始时缓存中还没有对热点数据进行缓存,所有请求都被发送到数据库去查询,数据库被压垮;
解决方案:
方案一:全局锁,在访问数据库之前都先请求全局锁,获得锁的线程才有资格去访问数据库,其他线程必须等待。由于现在的业务都是分布式的,本地锁没法控制其他服务器的线程也等待,所以要用全局锁,比如分布式锁;
方案二:热点数据,设置永不过期; 这种方案有两种实现方式:
实现方式一: Redis不设置热点key的过期时间,也就是“物理”不过期; 优点:简单; 缺点:缓存的热点数据是静态的,得不到更新;
实现方式二: Redis不设置热点key的过期时间,但把过期时间设置到key对应的value中,如果查询时发现过期了,通过一个后台异步线程进行缓存重建,即“逻辑”过期;
4.缓存雪崩
缓存雪崩是指: 1、在某一个时刻,大量的key或者整个缓存的数据全部过期了,然后瞬间所有的请求都落到数据库,数据库被压垮; 2、缓存发生了故障,导致所有的请求都落入到数据库,数据库被压垮;
缓存雪崩 和 缓存击穿 区别:
缓存击穿:高并发条件下,对于热点数据,当数据失效的一瞬间,或者刚开始时缓存中还没有对热点数据进行缓存,所有请求都被发送到数据库去查询,数据库被压垮;
缓存击穿强调的是 某些热点数据(或者说是同一条数据)过期了,而走数据库;
缓存雪崩强调的是 大量数据(或者说是所有数据)都过期了,而走数据库;
缓存雪崩的核心就是:你的缓存不行了,我都要走数据库;
1、我要查的数据缓存都没有;
2、缓存本身就不能用了,比如缓存宕机了;
解决方案:
1、Redis要高可用(搭建Redis Sentinel或者Redis Cluster集群),避免Redis不可用;
2、给不同的key设置不同的过期时间;
3、本地缓存(二级缓存) + 限流&降级,避免数据库被压垮;
5.Redis内存使用完了怎么办?
Redis的内存使用策略: redis.conf 配置文件 maxmemory 配置项,配置最大使用的内存限制; maxmemory = 0 表示无内存限制; 当内存达到maxmemory所设置的值时,可以使用如下内存淘汰策略:
默认的淘汰策略:noeviction
6.Redis的string类型的值最大能放多大的数据?
Redis的string类型的值:最大能放 512 MB;
Redis的List类型的值:最大能放 2^32 - 1 (4,294,967,295) 个元素;
Redis的Set类型的值:最大能放 2^32 - 1 (4,294,967,295) 个元素;
Redis的Hash类型的值:每个hash值最大能放 2^32 - 1 (4,294,967,295) 个field-value 对,Hash类型仅受部署Redis的服务器上的总内存的限制;
ZSet和Set一样;
7.如何保证数据库与Redis的数据一致性?
以上图片为线程不安全的情况出现的问题
1.先删除缓存再操作数据库
①不考虑线程安全问题:正常的情况下,我们首先会去删除缓存,然后再去更新数据库,这时候再去查询缓存,如果未命中就去查询数据库然后写入缓存。
②考虑线程安全问题:如果在我们删除缓存后,正好有另一个线程在查询缓存这时候就是未命中然后去查数据库紧接着写入缓存,然后主线程又去更新了数据库,这是就会出现缓存与数据库不一致的现象
2.先操作数据库,再删除缓存
①不考虑线程安全问题:正常情况下,我们先去更新数据库,然后删除缓存,然后才会有其他线程查询缓存,未命中的话就去查询数据库,最后写入缓存。
②考虑线程安全问题:如果此时刚好缓存key过期等其他问题导致缓存失效了,当有线程去查询缓存肯定是未命中然后去查数据库,然后另一个线程刚好在更新数据库,紧接着删除了缓存然后主线程才做的写入缓存操作,也会导致缓存与数据库不一致。
综上所述,第一种发生的概率比第二种概率要大很多,因为第二种需要同时满足三个条件才成立①刚好缓存失效②刚好在查完数据库是有另外线程更新数据库并删除缓存③刚好在另外线程更新数据库并删除缓存后写入缓存,所以使用第二种比较好。