SpringDataRedis

69次阅读

共计 10533 个字符,预计需要花费 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
lettuce:
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 不设失效时间。自己做缓存预热操作。然后细分以下几个小点

    1. 从缓存 A 读数据库,有则直接返回
    2. A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程。
    3. 更新线程同时更新缓存 A 和缓存 B。
正文完
 0