乐趣区

在Java中使用redisTemplate操作缓存

背景
在最近的项目中,有一个需求是对一个很大的数据库进行查询,数据量大概在几千万条。但同时对查询速度的要求也比较高。
这个数据库之前在没有使用 Presto 的情况下,使用的是 Hive,使用 Hive 进行一个简单的查询,速度可能在几分钟。当然几分钟也并不完全是跑 SQL 的时间,这里面包含发请求,查询数据并且返回数据的时间的总和。但是即使这样,这样的速度明显不能满足交互式的查询需求。
我们的下一个解决方案就是 Presto,在使用了 Presto 之后,查询速度降到了秒级。但是对于一个前端查询界面的交互式查询来说,十几秒仍然是一个不能接受的时间。
虽然 Presto 相比 Hive 已经快了很多(FaceBook 官方宣称的是 10 倍),但是对分页的支持不是很友好。我在使用的时候是自己在后端实现的分页。
在这种情况下应用缓存实属无奈之举。讲道理,优化应从底层开始,自底而上。上层优化的方式和效率感觉都很有局限。
<!–more–>
为什么要使用缓存
前端查询中,单次查询的匹配数据量有可能会达到上百甚至上千条,在前端中肯定是需要分页展示的。就算每次查询 10 条数据,整个查询也要耗时 6 -8s 的时间。想象一下,每翻一页等 10s 的场景。
所以,此时使用 redis 缓存。减少请求数据库的次数。将匹配的数据一并存入数据库。这样只有在第一次查询时耗费长一点,一旦查询完成,用户点击下一页就是毫秒级别的操作了。
使用 redisTemplate
Spring 封装了一个比较强大的模板,也就是 redisTemplate,方便在开发的时候操作 Redis 缓存。在 Redis 中可以存储 String、List、Set、Hash、Zset。下面将针对 List 和 Hash 分别介绍。
List
Redis 中的 List 为简单的字符串列表,常见的有下面几种操作。
hasKey
判断一个键是否存在,只需要调用 hasKey 就可以了。假设这个 Key 是 test,具体用法如下。
if (redisTemplate.hasKey(“test”)) {
System.out.println(“ 存在 ”);
} else {
System.out.println(“ 不存在 ”);
}
range
该函数用于从 redis 缓存中获取指定区间的数据。具体用法如下。
if (redisTemplate.hasKey(“test”)) {
// 该键的值为 [4, 3, 2, 1]
System.out.println(redisTemplate.opsForList().range(“test”, 0, 0)); // [4]
System.out.println(redisTemplate.opsForList().range(“test”, 0, 1)); // [4, 3]
System.out.println(redisTemplate.opsForList().range(“test”, 0, 2)); // [4, 3, 2]
System.out.println(redisTemplate.opsForList().range(“test”, 0, 3)); // [4, 3, 2, 1]
System.out.println(redisTemplate.opsForList().range(“test”, 0, 4)); // [4, 3, 2, 1]
System.out.println(redisTemplate.opsForList().range(“test”, 0, 5)); // [4, 3, 2, 1]

System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [4, 3, 2, 1] 如果结束位是 -1,则表示取所有的值
}
delete
删除某个键。
List<String> test = new ArrayList<>();
test.add(“1”);
test.add(“2”);
test.add(“3”);
test.add(“4”);

redisTemplate.opsForList().rightPushAll(“test”, test);
System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [1, 2, 3, 4]
redisTemplate.delete(“test”);
System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // []
size
获取该键的集合长度。
List<String> test = new ArrayList<>();
test.add(“1”);
test.add(“2”);
test.add(“3”);
test.add(“4”);

redisTemplate.opsForList().rightPushAll(“test”, test);
System.out.println(redisTemplate.opsForList().size(“test”)); // 4
leftPush
我们把存放这个值的地方想象成如图所示的容器。
container
并且取数据总是从左边取,但是存数据可以从左也可以从右。左就是 leftPush,右就是 rightPush。leftPush 如下图所示。
left-push
用法如下。
for (int i = 0; i < 4; i++) {
Integer value = i + 1;
redisTemplate.opsForList().leftPush(“test”, value.toString());
System.out.println(redisTemplate.opsForList().range(“test”, 0, -1));
}
控制台输出的结果如下。
[1]
[2, 1]
[3, 2, 1]
[4, 3, 2, 1]
leftPushAll
基本和 leftPush 一样,只不过是一次性的将 List 入栈。
List<String> test = new ArrayList<>();
test.add(“1”);
test.add(“2”);
test.add(“3”);
test.add(“4”);
redisTemplate.opsForList().leftPushAll(“test”, test);
System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [4, 3, 2, 1]
当然你也可以这样
redisTemplate.opsForList().leftPushAll(“test”, test);
System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [4, 3, 2, 1]
leftPushIfPresent
跟 leftPush 是同样的操作,唯一的不同是,当且仅当 key 存在时,才会更新 key 的值。如果 key 不存在则不会对数据进行任何操作。
redisTemplate.delete(“test”);

redisTemplate.opsForList().leftPushIfPresent(“test”, “1”);
redisTemplate.opsForList().leftPushIfPresent(“test”, “2”);
System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // []
leftPop
该函数用于移除上面我们抽象的容器中的最左边的一个元素。
List<String> test = new ArrayList<>();
test.add(“1”);
test.add(“2”);
test.add(“3”);
test.add(“4”);
redisTemplate.opsForList().rightPushAll(“test”, test);

redisTemplate.opsForList().leftPop(“test”); // [2, 3, 4]
redisTemplate.opsForList().leftPop(“test”); // [3, 4]
redisTemplate.opsForList().leftPop(“test”); // [4]
redisTemplate.opsForList().leftPop(“test”); // []
redisTemplate.opsForList().leftPop(“test”); // []
值得注意的是,当返回为空后,在 redis 中这个 key 也不复存在了。如果此时再调用 leftPushIfPresent,是无法再添加数据的。有代码有真相。
List<String> test = new ArrayList<>();
test.add(“1”);
test.add(“2”);
test.add(“3”);
test.add(“4”);
redisTemplate.opsForList().rightPushAll(“test”, test);

redisTemplate.opsForList().leftPop(“test”); // [2, 3, 4]
redisTemplate.opsForList().leftPop(“test”); // [3, 4]
redisTemplate.opsForList().leftPop(“test”); // [4]
redisTemplate.opsForList().leftPop(“test”); // []
redisTemplate.opsForList().leftPop(“test”); // []

redisTemplate.opsForList().leftPushIfPresent(“test”, “1”); // []
redisTemplate.opsForList().leftPushIfPresent(“test”, “1”); // []
rightPush
rightPush 如下图所示。
right-push
用法如下。
for (int i = 0; i < 4; i++) {
Integer value = i + 1;
redisTemplate.opsForList().leftPush(“test”, value.toString());
System.out.println(redisTemplate.opsForList().range(“test”, 0, -1));
}
控制台输出的结果如下。
[1]
[1, 2]
[1, 2, 3]
[1, 2, 3, 4]
rightPushAll
同 rightPush,一次性将 List 存入。
List<String> test = new ArrayList<>();
test.add(“1”);
test.add(“2”);
test.add(“3”);
test.add(“4”);
redisTemplate.opsForList().leftPushAll(“test”, test);
System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [1, 2, 3, 4]
当然你也可以这样。
redisTemplate.opsForList().rightPushAll(“test”, “1”, “2”, “3”, “4”);
System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [1, 2, 3, 4]
rightPushIfPresent
跟 rightPush 是同样的操作,唯一的不同是,当且仅当 key 存在时,才会更新 key 的值。如果 key 不存在则不会对数据进行任何操作。
redisTemplate.delete(“test”);

redisTemplate.opsForList().rightPushIfPresent(“test”, “1”);
redisTemplate.opsForList().rightPushIfPresent(“test”, “2”);
System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // []
rightPop
该函数用于移除上面我们抽象的容器中的最右边的一个元素。
List<String> test = new ArrayList<>();
test.add(“1”);
test.add(“2”);
test.add(“3”);
test.add(“4”);
redisTemplate.opsForList().rightPushAll(“test”, test);

redisTemplate.opsForList().rightPop(“test”); // [1, 2, 3]
redisTemplate.opsForList().rightPop(“test”); // [1, 2]
redisTemplate.opsForList().rightPop(“test”); // [1]
redisTemplate.opsForList().rightPop(“test”); // []
redisTemplate.opsForList().rightPop(“test”); // []
与 leftPop 一样,返回空之后,再调用 rightPushIfPresent,是无法再添加数据的。
index
获取 list 中指定位置的元素。
if (redisTemplate.hasKey(“test”)) {
// 该键的值为 [1, 2, 3, 4]
System.out.println(redisTemplate.opsForList().index(“test”, -1)); // 4
System.out.println(redisTemplate.opsForList().index(“test”, 0)); // 1
System.out.println(redisTemplate.opsForList().index(“test”, 1)); // 2
System.out.println(redisTemplate.opsForList().index(“test”, 2)); // 3
System.out.println(redisTemplate.opsForList().index(“test”, 3)); // 4
System.out.println(redisTemplate.opsForList().index(“test”, 4)); // null
System.out.println(redisTemplate.opsForList().index(“test”, 5)); // null
}
值得注意的有两点。一个是如果下标是 - 1 的话,则会返回 List 最后一个元素,另一个如果数组下标越界,则会返回 null。
trim
用于截取指定区间的元素,可能你会理解成与 range 是一样的作用。看了下面的代码之后应该就会立刻理解。
List<String> test = new ArrayList<>();
test.add(“1”);
test.add(“2”);
test.add(“3”);
test.add(“4”);
redisTemplate.opsForList().rightPushAll(“test”, test); // [1, 2, 3, 4]

redisTemplate.opsForList().trim(“test”, 0, 2); // [1, 2, 3]
其实作用完全不一样。range 是获取指定区间内的数据,而 trim 是留下指定区间的数据,删除不在区间的所有数据。trim 是 void,不会返回任何数据。
remove
用于移除键中指定的元素。接受 3 个参数,分别是缓存的键名,计数事件,要移除的值。计数事件可以传入的有三个值,分别是 -1、0、1。
- 1 代表从存储容器的最右边开始,删除一个与要移除的值匹配的数据;0 代表删除所有与传入值匹配的数据;1 代表从存储容器的最左边开始,删除一个与要移除的值匹配的数据。
List<String> test = new ArrayList<>();
test.add(“1”);
test.add(“2”);
test.add(“3”);
test.add(“4”);
test.add(“4”);
test.add(“3”);
test.add(“2”);
test.add(“1”);

redisTemplate.opsForList().rightPushAll(“test”, test); // [1, 2, 3, 4, 4, 3, 2, 1]

// 当计数事件是 -1、传入值是 1 时
redisTemplate.opsForList().remove(“test”, -1, “1”); // [1, 2, 3, 4, 4, 3, 2]

// 当计数事件是 1,传入值是 1 时
redisTemplate.opsForList().remove(“test”, 1, “1”); // [2, 3, 4, 4, 3, 2]

// 当计数事件是 0,传入值是 4 时
redisTemplate.opsForList().remove(“test”, 0, “4”); // [2, 3, 3, 2]
rightPopAndLeftPush
该函数用于操作两个键之间的数据,接受三个参数,分别是源 key、目标 key。该函数会将源 key 进行 rightPop,再将返回的值,作为输入参数,在目标 key 上进行 leftPush。具体代码如下。
List<String> test = new ArrayList<>();
test.add(“1”);
test.add(“2”);
test.add(“3”);
test.add(“4”);

List<String> test2 = new ArrayList<>();
test2.add(“1”);
test2.add(“2”);
test2.add(“3”);

redisTemplate.opsForList().rightPushAll(“test”, test); // [1, 2, 3, 4]
redisTemplate.opsForList().rightPushAll(“test2”, test2); // [1, 2, 3]

redisTemplate.opsForList().rightPopAndLeftPush(“test”, “test2”);

System.out.println(redisTemplate.opsForList().range(“test”, 0, -1)); // [1, 2, 3]
System.out.println(redisTemplate.opsForList().range(“test2”, 0, -1)); // [4, 1, 2, 3]
Hash
存储类型为 hash 其实很好理解。在上述的 List 中,一个 redis 的 Key 可以理解为一个 List,而在 Hash 中,一个 redis 的 Key 可以理解为一个 HashMap。
put
用于写入数据。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);

redisTemplate.opsForHash().put(“test”, “map”, list.toString()); // [1, 2, 3, 4]
redisTemplate.opsForHash().put(“test”, “isAdmin”, true); // true
putALl
用于一次性向一个 Hash 键中添加多个 key。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);
List<String> list2 = new ArrayList<>();
list2.add(“5”);
list2.add(“6”);
list2.add(“7”);
list2.add(“8”);
Map<String, String> valueMap = new HashMap<>();
valueMap.put(“map1”, list.toString());
valueMap.put(“map2”, list2.toString());

redisTemplate.opsForHash().putAll(“test”, valueMap); // {map2=[5, 6, 7, 8], map1=[1, 2, 3, 4]}
putIfAbsent
用于向一个 Hash 键中写入数据。当 key 在 Hash 键中已经存在时,则不会写入任何数据,只有在 Hash 键中不存在这个 key 时,才会写入数据。
同时,如果连这个 Hash 键都不存在,redisTemplate 会新建一个 Hash 键,再写入 key。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);
redisTemplate.opsForHash().putIfAbsent(“test”, “map”, list.toString());
System.out.println(redisTemplate.opsForHash().entries(“test”)); // {map=[1, 2, 3, 4]}
get
用于获取数据。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);

redisTemplate.opsForHash().put(“test”, “map”, list.toString());
redisTemplate.opsForHash().put(“test”, “isAdmin”, true);

System.out.println(redisTemplate.opsForHash().get(“test”, “map”)); // [1, 2, 3, 4]
System.out.println(redisTemplate.opsForHash().get(“test”, “isAdmin”)); // true

Boolean bool = (Boolean) redisTemplate.opsForHash().get(“test”, “isAdmin”);
System.out.println(bool); // true

String str = redisTemplate.opsForHash().get(“test”, “map”).toString();
List<String> array = JSONArray.parseArray(str, String.class);
System.out.println(array.size()); // 4
值得注意的是,使用 get 函数获取的数据都是 Object 类型。
所以需要使用类型与上述例子中的布尔类型的话,则需要强制转换一次。List 类型则可以使用 fastjson 这种工具来进行转换。转换的例子已列举在上述代码中。
delete
用于删除一个 Hash 键中的 key。可以理解为删除一个 map 中的某个 key。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);
List<String> list2 = new ArrayList<>();
list2.add(“5”);
list2.add(“6”);
list2.add(“7”);
list2.add(“8”);
Map<String, String> valueMap = new HashMap<>();
valueMap.put(“map1”, list.toString());
valueMap.put(“map2”, list2.toString());

redisTemplate.opsForHash().putAll(“test”, valueMap); // {map2=[5, 6, 7, 8], map1=[1, 2, 3, 4]}
redisTemplate.opsForHash().delete(“test”, “map1”); // {map2=[5, 6, 7, 8]}
values
用于获取一个 Hash 类型的键的所有值。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);

redisTemplate.opsForHash().put(“test”, “map”, list.toString());
redisTemplate.opsForHash().put(“test”, “isAdmin”, true);

System.out.println(redisTemplate.opsForHash().values(“test”)); // [[1, 2, 3, 4], true]
entries
用于以 Map 的格式获取一个 Hash 键的所有值。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);

redisTemplate.opsForHash().put(“test”, “map”, list.toString());
redisTemplate.opsForHash().put(“test”, “isAdmin”, true);

Map<String, String> map = redisTemplate.opsForHash().entries(“test”);
System.out.println(map.get(“map”)); // [1, 2, 3, 4]
System.out.println(map.get(“map”) instanceof String); // true
System.out.println(redisTemplate.opsForHash().entries(“test”)); // {a=[1, 2, 3, 4], isAdmin=true}
hasKey
用于获取一个 Hash 键中是否含有某个键。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);

redisTemplate.opsForHash().put(“test”, “map”, list.toString());
redisTemplate.opsForHash().put(“test”, “isAdmin”, true);

System.out.println(redisTemplate.opsForHash().hasKey(“test”, “map”)); // true
System.out.println(redisTemplate.opsForHash().hasKey(“test”, “b”)); // false
System.out.println(redisTemplate.opsForHash().hasKey(“test”, “isAdmin”)); // true
keys
用于获取一个 Hash 键中所有的键。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);

redisTemplate.opsForHash().put(“test”, “map”, list.toString());
redisTemplate.opsForHash().put(“test”, “isAdmin”, true);

System.out.println(redisTemplate.opsForHash().keys(“test”)); // [a, isAdmin]
size
用于获取一个 Hash 键中包含的键的数量。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);

redisTemplate.opsForHash().put(“test”, “map”, list.toString());
redisTemplate.opsForHash().put(“test”, “isAdmin”, true);

System.out.println(redisTemplate.opsForHash().size(“test”)); // 2
increment
用于让一个 Hash 键中的某个 key,根据传入的值进行累加。传入的数值只能是 double 或者 long,不接受浮点型
redisTemplate.opsForHash().increment(“test”, “a”, 3);
redisTemplate.opsForHash().increment(“test”, “a”, -3);
redisTemplate.opsForHash().increment(“test”, “a”, 1);
redisTemplate.opsForHash().increment(“test”, “a”, 0);

System.out.println(redisTemplate.opsForHash().entries(“test”)); // {a=1}
multiGet
用于批量的获取一个 Hash 键中多个 key 的值。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);
List<String> list2 = new ArrayList<>();
list2.add(“5”);
list2.add(“6”);
list2.add(“7”);
list2.add(“8”);

redisTemplate.opsForHash().put(“test”, “map1”, list.toString()); // [1, 2, 3, 4]
redisTemplate.opsForHash().put(“test”, “map2”, list2.toString()); // [5, 6, 7, 8]

List<String> keys = new ArrayList<>();
keys.add(“map1”);
keys.add(“map2”);

System.out.println(redisTemplate.opsForHash().multiGet(“test”, keys)); // [[1, 2, 3, 4], [5, 6, 7, 8]]
System.out.println(redisTemplate.opsForHash().multiGet(“test”, keys) instanceof List); // true
scan
获取所以匹配条件的 Hash 键中 key 的值。我查过一些资料,大部分写的是无法模糊匹配,我自己尝试了一下,其实是可以的。如下,使用 scan 模糊匹配 hash 键的 key 中,带 SCAN 的 key。
List<String> list = new ArrayList<>();
list.add(“1”);
list.add(“2”);
list.add(“3”);
list.add(“4”);
List<String> list2 = new ArrayList<>();
list2.add(“5”);
list2.add(“6”);
list2.add(“7”);
list2.add(“8”);
List<String> list3 = new ArrayList<>();
list3.add(“9”);
list3.add(“10”);
list3.add(“11”);
list3.add(“12”);
Map<String, String> valueMap = new HashMap<>();
valueMap.put(“map1”, list.toString());
valueMap.put(“SCAN_map2”, list2.toString());
valueMap.put(“map3”, list3.toString());

redisTemplate.opsForHash().putAll(“test”, valueMap); // {SCAN_map2=[5, 6, 7, 8], map3=[9, 10, 11, 12], map1=[1, 2, 3, 4]}

Cursor<Map.Entry<String, String>> cursor = redisTemplate.opsForHash().scan(“test”, ScanOptions.scanOptions().match(“*SCAN*”).build());
if (cursor.hasNext()) {
while (cursor.hasNext()) {
Map.Entry<String, String> entry = cursor.next();
System.out.println(entry.getValue()); // [5, 6, 7, 8]
}
}
引入 redisTemplate
如果大家看懂了怎么用,就可以将 redisTemplate 引入项目中了。
引入 pom 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
新建配置文件
然后需要新建一个 RedisConfig 配置文件。
package com.detectivehlh;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

/**
* RedisConfig
*
* @author Lunhao Hu
* @date 2019-01-17 15:12
**/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//redis 序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(om);

StringRedisTemplate template = new StringRedisTemplate(factory);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
注入
将 redisTemplate 注入到需要使用的地方。
@Autowired
private RedisTemplate redisTemplate;
写在后面
Github

退出移动版