一、Redis应用场景
1.1、缓存
热点数据(高频查询,但不经常修改和删除的数据)首选redis作为缓存,性能优秀。
案例:如仓储业务中的商品信息,用户从redis的查询商品信息,没有在去数据库中查询。
1.2、分布式锁
在多线程环境下,对共享资源访问的线程问题,需要通过锁的机制实现访问的互斥。
案例:在集群环境下的库存扣减功能,普通方式实现,步骤:①、获取锁:setnx key vlue ②、设置好锁的过期时间:expire key 10 ③、执行业务代码 ④、释放锁:del key
使用redisson来实现分布式锁:①、获取锁 ②、执行业务代码 ③、释放锁
redisson加锁、锁续期、释放锁底层都是通过lua脚本实现的,这样保证了操作的原子性。
redisson的锁为可重入锁。
1.2.1、分布式锁的坑 -- 非原子操作
解决办法:①、将加锁与设置锁的过期时间进行统一操作。②采用lua脚本。
1.2.2、分布式锁的坑 -- 锁未释放
解决办法:锁必须加过期时间。
1.2.3、分布式锁的坑 -- 锁提前释放
解决办法:给锁续期
1.2.4、分布式锁的坑 -- 释放其他线程的锁
解决办法:给每个锁添加唯一的id
1.2.5、分布式锁的坑 -- 大量请求竞争锁失败
解决办法:①、使用自旋锁重试。②、优化业务代码,将执行时间变短。③、进行限流。
1.2.6、分布式锁的坑 -- 主从复制
解决办法:①、红锁机制(不采用)
1.2.7、分布式锁的坑 -- 锁的性能
解决办法:分段锁(大多数不采用,需要根据场景)
1.2.8、分布式锁的坑 -- 锁可重入性
解决办法:redisson自身实现
1.3、使用场景案例
1.3.1、Token存储
过程:前端发送登录请求给后端,后端生成一个token返回给前端,并存储在redis中。当前端发送请求是携带token需要给跟redis存储中token进行验证,通过才能返回结果给前端。
1.3.2、短信验证码存储
过程:用户发送获取短信验证码,后端获取并写入redis中,前端用户输入验证码与redis中进行比较。
1.3.3、计数器
如:文章阅读数,视频点赞数等,使用hset类型。
1.3.4、全局唯一ID
主要使用redis自增id的特性。
1.3.5、排行榜
如积分排行榜,英雄战力排行榜。使用zset类型数据。
1.3.6、限流
local key = KEYS[1] --限流KEY(一秒一个)"limit:" + System.currentTimeMillis() / 1000;
local limit = tonumber(ARGV[1]) --限流大小 10
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
return 0
else --请求数+1,并设置2秒过期
redis.call("INCRBY", key, "1")
redis.call("EXPIRE", key, "15")
return 1
end
@Test
void testLimit() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
for (int i = 0; i < 112; i++) {
String key = "limit:" + System.currentTimeMillis() / 1000; // limit:1705664721
Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(key), String.valueOf(100));
if (result == 0) {
//需要限流
System.out.println("限流了......");
}
}
}
1.3.7、购物车
使用hset类型实现
1.4、缓存穿透
原因:用户访问一个不存在的数据,而redis就会去请求后端数据库,当这种大量不存在的数据访问,导致redis频繁去查询后端数库以至于后端数据库崩溃。
解决办法:①、缓存空结果,将所有查询的数据都存在redis的缓存中,减少对数据库的查询,优点:实现简单。缺点:会导致redis的内存被大量占用,出现缓存与数据库不一致的情况。
②、布隆过滤器。优点:不会缓存无效数据,缺点:实现比较复杂,存在一定误判。
1.4.1、布隆过滤器
使用方案:在用户请求reids缓存之前,先经过布隆过滤器,经过筛选在去查询redis缓存。
原理:布隆过滤器主要使用一个很长的二进制数组,在由一系列hsah函数来确定请求数据是否存在于该集合中。
1.5、缓存击穿
原因:在高并发条件下,对于热点数据,当热点数据失效的瞬间,或刚开始还没有对热点数据进行缓存,所有请求都被发送到数据库去查询,数据库就会崩溃。
解决办法:①、使用全局锁:在访问数据库之前都先请求全局锁,获取锁的线程才有资格去访问数据库,其他线程必须等待。由于现在的业务都是分布式的,本地锁没法控制其他服务器的线程等待,所以要用全局锁。如分布式锁。
②、热点数据,设置不过期时间。实现方式一:redis中的热点key不设置过期时间,缺点:缓存热点数据是静态的,得不到更新。实现方式二:给热点key设置一个逻辑过期时间得字段,跟逻辑删除思路一致。
1.6、缓存雪崩
原因:①、由于大量得key或整个缓存数据全部过期,同时又有大量请求访问这些过期缓存而导致请求落地数据库上,而导致数据库崩溃。②、缓存发生故障,导致大量请求落入到数据库,导致数据崩溃。
解决办法:①、建立高可用得redis集群。②、给不同的key设置不同的过期时间。③、本地缓存+限流或降级,避免数据库被压垮。
1.7、内存使用完怎么办?
1.8、String类型的值最大能放多大数据?
String类型:512MB
List,Set,Hash类型:2^32-1(4294967295) 个元素。
1.9、如何保证数据库与redis的数据一致性?
解决办法:使用缓存双写一致性
1.10、redis集群最大能部署多少主节点?
redis集群中内置了16384个哈希槽,redis会根据节点数量大致均等的将哈希槽映射到不同节点
二、Spring
2.1、Spring容器中的bean是线程安全的吗?
答:不是线程安全的。
原因:①、spring容器中的bean默认是singleton单例的,所有线程都共享一个单例bean,因此是存在资源竞争的。②、实际开发中,单例bean一般以无状态的方式来使用,即线程之间的操作不会对bean的成员执行除查询以外的操作,所以这个单例bean也可以说是线程安全的。如c