共计 10513 个字符,预计需要花费 27 分钟才能阅读完成。
SpringDataRedis
创立我的项目
增加依赖
<dependencies>
<!-- spring data redis 组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 对象池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- web 组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- test 组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
增加 application.yml 配置文件
spring:
redis:
# Redis 服务器地址
host: 192.168.10.100
# Redis 服务器端口
port: 6379
# Redis 服务器端口
password: root
# Redis 服务器端口
database: 0
# 连贯超时工夫
timeout: 10000ms
jedis:
pool:
# 最大连接数,默认 8
max-active: 1024
# 最大连贯阻塞等待时间,单位毫秒,默认 -1ms
max-wait: 10000ms
# 最大闲暇连贯,默认 8
max-idle: 200
# 最小闲暇连贯,默认 0
min-idle: 5
Lettuce 和 Jedis 的区别
Jedis
是一个优良的基于 Java 语言的 Redis 客户端,然而,其有余也很显著:Jedis
在实现上是间接连贯 Redis-Server,在多个线程间共享一个 Jedis
实例时是线程不平安的,如果想要在多线程场景下应用 Jedis
,须要应用连接池,每个线程都应用本人的 Jedis
实例,当连贯数量增多时,会耗费较多的物理资源。
Lettuce
则齐全克服了其线程不平安的毛病:Lettuce
是基于 Netty
的连贯(StatefulRedisConnection),
Lettuce
是一个可伸缩的线程平安的 Redis 客户端,反对同步、异步和响应式模式。多个线程能够共享一个连贯实例,而不用放心多线程并发问题。它基于优良 Netty NIO 框架构建,反对 Redis 的高级性能,如 Sentinel,集群,流水线,主动从新连贯和 Redis 数据模型。
测试环境测试环境是否搭建胜利
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringDataRedisApplication.class)
public class SpringDataRedisApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void initconn() {ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("username","lisi");
ValueOperations<String, String> value = redisTemplate.opsForValue();
value.set("name","wangwu");
System.out.println(ops.get("name"));
}
}
自定义模板解决序列化问题
默认状况下的模板 RedisTemplate<Object, Object>,默认序列化应用的是JdkSerializationRedisSerializer
,存储二进制字节码。这时须要自定义模板,当自定义模板后又想存储 String 字符串时,能够使 StringRedisTemplate 的形式,他们俩并不抵触。
序列化问题:
要把 domain object 做为 key-value 对保留在 redis 中,就必须要解决对象的序列化问题。Spring Data Redis 给咱们提供了一些现成的计划:
JdkSerializationRedisSerializer
应用 JDK 提供的序列化性能。长处是反序列化时不须要提供类型信息(class),但毛病是序列化后的后果十分宏大,是 JSON 格局的 5 倍左右,这样就会耗费 Redis 服务器的大量内存。
Jackson2JsonRedisSerializer
应用 Jackson 库将对象序列化为 JSON 字符串。长处是速度快,序列化后的字符串短小精悍。但毛病也十分致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class 对象)。通过查看源代码,发现其只在反序列化过程中用到了类型信息。
GenericJackson2JsonRedisSerializer
通用型序列化,这种序列化形式不必本人手动指定对象的 Class。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory){RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
// 为 string 类型 key 设置序列器
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 为 string 类型 value 设置序列器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 为 hash 类型 key 设置序列器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 为 hash 类型 value 设置序列器
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
// 序列化
@Test
public void testSerial(){User user = new User();
user.setId(1);
user.setUsername("张三");
user.setPassword("111");
ValueOperations<String, Object> value = redisTemplate.opsForValue();
value.set("userInfo",user);
System.out.println(value.get("userInfo"));
}
操作 string
// 1. 操作 String
@Test
public void testString() {ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
// 增加一条数据
valueOperations.set("username", "zhangsan");
valueOperations.set("age", "18");
// redis 中以层级关系、目录模式存储数据
valueOperations.set("user:01", "lisi");
valueOperations.set("user:02", "wangwu");
// 增加多条数据
Map<String, String> userMap = new HashMap<>();
userMap.put("address", "bj");
userMap.put("sex", "1");
valueOperations.multiSet(userMap);
// 获取一条数据
Object username = valueOperations.get("username");
System.out.println(username);
// 获取多条数据
List<String> keys = new ArrayList<>();
keys.add("username");
keys.add("age");
keys.add("address");
keys.add("sex");
List<Object> resultList = valueOperations.multiGet(keys);
for (Object str : resultList) {System.out.println(str);
}
// 删除
redisTemplate.delete("username");
}
操作 hash
// 2. 操作 Hash
@Test
public void testHash() {HashOperations<String, String, String> hashOperations = redisTemplate.opsForHash();
/*
* 增加一条数据
* 参数一:redis 的 key
* 参数二:hash 的 key
* 参数三:hash 的 value
*/
hashOperations.put("userInfo","name","lisi");
// 增加多条数据
Map<String, String> map = new HashMap();
map.put("age", "20");
map.put("sex", "1");
hashOperations.putAll("userInfo", map);
// 获取一条数据
String name = hashOperations.get("userInfo", "name");
System.out.println(name);
// 获取多条数据
List<String> keys = new ArrayList<>();
keys.add("age");
keys.add("sex");
List<String> resultlist =hashOperations.multiGet("userInfo", keys);
for (String str : resultlist) {System.out.println(str);
}
// 获取 Hash 类型所有的数据
Map<String, String> userMap = hashOperations.entries("userInfo");
for (Entry<String, String> userInfo : userMap.entrySet()) {System.out.println(userInfo.getKey() + "--" + userInfo.getValue());
}
// 删除 用于删除 hash 类型数据
hashOperations.delete("userInfo", "name");
}
操作 list
// 3. 操作 list
@Test
public void testList() {ListOperations<String, Object> listOperations = redisTemplate.opsForList();
// 左增加(上)
// listOperations.leftPush("students", "Wang Wu");
// listOperations.leftPush("students", "Li Si");
// 左增加(上) 把 value 值放到 key 对应列表中 pivot 值的右面,如果 pivot 值存在的话
//listOperations.leftPush("students", "Wang Wu", "Li Si");
// 右增加(下)
// listOperations.rightPush("students", "Zhao Liu");
// 获取 start 起始下标 end 完结下标 蕴含关系
List<Object> students = listOperations.range("students", 0,2);
for (Object stu : students) {System.out.println(stu);
}
// 依据下标获取
Object stu = listOperations.index("students", 1);
System.out.println(stu);
// 获取总条数
Long total = listOperations.size("students");
System.out.println("总条数:" + total);
// 删除单条 删除列表中存储的列表中几个呈现的 Li Si。listOperations.remove("students", 1, "Li Si");
// 删除多条
redisTemplate.delete("students");
}
操作 set
// 4. 操作 set- 无序
@Test
public void testSet() {SetOperations<String, Object> setOperations = redisTemplate.opsForSet();
// 增加数据
String[] letters = new String[]{"aaa", "bbb", "ccc", "ddd", "eee"};
//setOperations.add("letters", "aaa", "bbb", "ccc", "ddd", "eee");
setOperations.add("letters", letters);
// 获取数据
Set<Object> let = setOperations.members("letters");
for (Object letter: let) {System.out.println(letter);
}
// 删除
setOperations.remove("letters", "aaa", "bbb");
}
操作 sorted set
// 5. 操作 sorted set- 有序
@Test
public void testSortedSet() {ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();
ZSetOperations.TypedTuple<Object> objectTypedTuple1 =
new DefaultTypedTuple<Object>("zhangsan", 7D);
ZSetOperations.TypedTuple<Object> objectTypedTuple2 =
new DefaultTypedTuple<Object>("lisi", 3D);
ZSetOperations.TypedTuple<Object> objectTypedTuple3 =
new DefaultTypedTuple<Object>("wangwu", 5D);
ZSetOperations.TypedTuple<Object> objectTypedTuple4 =
new DefaultTypedTuple<Object>("zhaoliu", 6D);
ZSetOperations.TypedTuple<Object> objectTypedTuple5 =
new DefaultTypedTuple<Object>("tianqi", 2D);
Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<ZSetOperations.TypedTuple<Object>>();
tuples.add(objectTypedTuple1);
tuples.add(objectTypedTuple2);
tuples.add(objectTypedTuple3);
tuples.add(objectTypedTuple4);
tuples.add(objectTypedTuple5);
// 增加数据
zSetOperations.add("score", tuples);
// 获取数据
Set<Object> scores = zSetOperations.range("score", 0, 4);
for (Object score: scores) {System.out.println(score);
}
// 获取总条数
Long total = zSetOperations.size("score");
System.out.println("总条数:" + total);
// 删除
zSetOperations.remove("score", "zhangsan", "lisi");
}
获取所有 key& 删除
// 获取所有 key
@Test
public void testAllKeys() {
// 以后库 key 的名称
Set<String> keys = redisTemplate.keys("*");
for (String key: keys) {System.out.println(key);
}
}
// 删除
@Test
public void testDelete() {
// 删除 通用 实用于所有数据类型
redisTemplate.delete("score");
}
设置 key 的生效工夫
@Test
public void testEx() {ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
// 办法一:插入一条数据并设置生效工夫
valueOperations.set("code", "abcd", 180, TimeUnit.SECONDS);
// 办法二:给已存在的 key 设置生效工夫
boolean flag = redisTemplate.expire("code", 180, TimeUnit.SECONDS);
// 获取指定 key 的生效工夫
Long l = redisTemplate.getExpire("code");
}
SpringDataRedis 整合应用哨兵机制
application.yml
spring:
redis:
# Redis 服务器地址
host: 192.168.10.100
# Redis 服务器端口
port: 6379
# Redis 服务器端口
password: root
# Redis 服务器端口
database: 0
# 连贯超时工夫
timeout: 10000ms
lettuce:
pool:
# 最大连接数,默认 8
max-active: 1024
# 最大连贯阻塞等待时间,单位毫秒,默认 -1ms
max-wait: 10000ms
# 最大闲暇连贯,默认 8
max-idle: 200
# 最小闲暇连贯,默认 0
min-idle: 5
#哨兵模式
sentinel:
#主节点名称
master: mymaster
#节点
nodes: 192.168.10.100:26379,192.168.10.100:26380,192.168.10.100:26381
Bean 注解配置
@Bean
public RedisSentinelConfiguration redisSentinelConfiguration(){RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
// 主节点名称
.master("mymaster")
// 主从服务器地址
.sentinel("192.168.10.100", 26379)
.sentinel("192.168.10.100", 26380)
.sentinel("192.168.10.100", 26381);
// 设置明码
sentinelConfig.setPassword("root");
return sentinelConfig;
}
如何应答缓存穿透、缓存击穿、缓存雪崩问题
Key 的过期淘汰机制
Redis 能够对存储在 Redis 中的缓存数据设置过期工夫,比方咱们获取的短信验证码个别十分钟过期,咱们这时候就须要在验证码存进 Redis 时增加一个 key 的过期工夫,然而这里有一个须要分外留神的问题就是:并非 key 过期工夫到了就肯定会被 Redis 给删除。
定期删除
Redis 默认是每隔 100ms 就随机抽取一些设置了过期工夫的 Key,查看其是否过期,如果过期就删除。为什么是随机抽取而不是查看所有 key?因为你如果设置的 key 成千上万,每 100 毫秒都将所有存在的 key 查看一遍,会给 CPU 带来比拟大的压力。
惰性删除
定期删除因为是随机抽取可能会导致很多过期 Key 到了过期工夫并没有被删除。所以用户在从缓存获取数据的时候,redis 会查看这个 key 是否过期了,如果过期就删除这个 key。这时候就会在查问的时候将过期 key 从缓存中革除。
内存淘汰机制
仅仅应用定期删除 + 惰性删除机制还是会留下一个重大的隐患:如果定期删除留下了很多曾经过期的 key,而且用户长时间都没有应用过这些过期 key,导致过期 key 无奈被惰性删除,从而导致过期 key 始终沉积在内存里,最终造成 Redis 内存块被耗费殆尽。那这个问题如何解决呢?这个时候 Redis 内存淘汰机制应运而生了。Redis 内存淘汰机制提供了 6 种数据淘汰策略:
volatile-lru
:从已设置过期工夫的数据集中筛选最近起码应用的数据淘汰。volatile-ttl
:从已设置过期工夫的数据集中筛选将要过期的数据淘汰。volatile-random
:从已设置过期工夫的数据集中任意抉择数据淘汰。allkeys-lru
:当内存不足以包容新写入数据时移除最近起码应用的 key。allkeys-random
:从数据集中任意抉择数据淘汰。no-enviction(默认)
:当内存不足以包容新写入数据时,新写入操作会报错。
个别状况下,举荐应用 volatile-lru
策略,对于配置信息等重要数据,不应该设置过期工夫,这样 Redis 就永远不会淘汰这些重要数据。对于个别数据能够增加一个缓存工夫,当数据生效则申请会从 DB 中获取并从新存入 Redis 中。
缓存击穿
首先咱们来看下申请是如何取到数据的:当接管到用户申请,首先先尝试从 Redis 缓存中获取到数据,如果缓存中能取到数据则间接返回后果,当缓存中不存在数据时从 DB 获取数据,如果数据库胜利取到数据,则更新 Redis,而后返回数据
定义:高并发的状况下,某个热门 key 忽然过期,导致大量申请在 Redis 未找到缓存数据,进而全副去拜访 DB 申请数据,引起 DB 压力霎时增大。
解决方案:缓存击穿的状况下个别不容易造成 DB 的宕机,只是会造成对 DB 的周期性压力。对缓存击穿的解决方案个别能够这样:
- Redis 中的数据不设置过期工夫,而后在缓存的对象上增加一个属性标识过期工夫,每次获取到数据时,校验对象中的过期工夫属性,如果数据行将过期,则异步发动一个线程被动更新缓存中的数据。然而这种计划可能会导致有些申请会拿到过期的值,就得看业务是否能够承受,
- 如果要求数据必须是新数据,则最好的计划则为热点数据设置为永不过期,而后加一个互斥锁保障缓存的单线程写。
缓存穿透
定义:缓存穿透是指查问缓存和 DB 中都不存在的数据。比方通过 id 查问商品信息,id 个别大于 0,攻击者会成心传 id 为 - 1 去查问,因为缓存是不命中则从 DB 中获取数据,这将会导致每次缓存都不命中数据导致每个申请都拜访 DB,造成缓存穿透。
解决方案:
- 利用互斥锁,缓存生效的时候,先去取得锁,失去锁了,再去申请数据库。没失去锁,则休眠一段时间重试
- 采纳异步更新策略,无论 key 是否取到值,都间接返回。value 值中保护一个缓存生效工夫,缓存如果过期,异步起一个线程去读数据库,更新缓存。须要做缓存预热 (我的项目启动前,先加载缓存) 操作。
- 提供一个能迅速判断申请是否无效的拦挡机制,比方,利用布隆过滤器,外部保护一系列非法无效的 key。迅速判断出,申请所携带的 Key 是否非法无效。如果不非法,则间接返回。
- 如果从数据库查问的对象为空,也放入缓存,只是设定的缓存过期工夫较短,比方设置为 60 秒。
缓存雪崩
定义:缓存中如果大量缓存在一段时间内集中过期了,这时候会产生大量的缓存击穿景象,所有的申请都落在了 DB 上,因为查问数据量微小,引起 DB 压力过大甚至导致 DB 宕机。
解决方案:
- 给缓存的生效工夫,加上一个随机值,防止个体生效。如果 Redis 是集群部署,将热点数据均匀分布在不同的 Redis 库中也能防止全副生效的问题
- 应用互斥锁,然而该计划吞吐量显著降落了。
- 设置热点数据永远不过期。
-
双缓存。咱们有两个缓存,缓存 A 和缓存 B。缓存 A 的生效工夫为 20 分钟,缓存 B 不设生效工夫。本人做缓存预热操作。而后细分以下几个小点
- 从缓存 A 读数据库,有则间接返回
- A 没有数据,间接从 B 读数据,间接返回,并且异步启动一个更新线程。
- 更新线程同时更新缓存 A 和缓存 B。
Redis 学习视频!!!