Redis
是⽬前使⽤最⼴泛的缓存中间件,相⽐
Memcached
,
Redis
⽀持更多的数据结构和更丰富的数据操
作,另外
Redis
有着丰富的集群⽅案和使⽤场景,这⼀课我们⼀起学习
Redis
的常⽤操作。
Redis 介绍
Redis
是⼀个速度⾮常快的⾮关系数据库(
Non-Relational Database
),它可以存储键(
Key
)与
5
种不同
类型的值(
Value
)之间的映射(
Mapping
),可以将存储在内存的键值对数据持久化到硬盘,可以使⽤复制
特性来扩展读性能,还可以使⽤客户端分⽚来扩展写性能。
为了满⾜⾼性能,
Redis
采⽤内存(
in-memory
)数据集(
Dataset
),根据使⽤场景,可以通过每隔⼀段时
间转储数据集到磁盘,或者追加每条命令到⽇志来持久化。持久化也可以被禁⽤,如果你只是需要⼀个功能
丰富、⽹络化的内存缓存。
数据模型
Redis
数据模型不仅与关系数据库管理系统(
RDBMS
)不同,也不同于任何简单的
NoSQL
键
-
值数据存储。
Redis
数据类型类似于编程语⾔的基础数据类型,因此开发⼈员感觉很⾃然,每个数据类型都⽀持适⽤于其
类型的操作,受⽀持的数据类型包括:
- string(字符串)
- hash(哈希)
- list(列表)
- set(集合)
- zset(sorted set:有序集合)
关键优势
Redis
的优势包括它的速度、对富数据类型的⽀持、操作的原⼦性,以及通⽤性:
- 性能极⾼,它每秒可执⾏约 100,000 个 SET 以及约 100,000 个 GET 操作;
- 丰富的数据类型,Redis 对⼤多数开发⼈员已知的⼤多数数据类型提供了原⽣⽀持,这使得各种问题得 以轻松解决;
- 原⼦性,因为所有 Redis 操作都是原⼦性的,所以多个客户端会并发地访问⼀个 Redis 服务器,获取相 同的更新值;
- 丰富的特性,Redis 是⼀个多效⽤⼯具,有⾮常多的应⽤场景,包括缓存、消息队列(Redis 原⽣⽀持 发布/订阅)、短期应⽤程序数据(⽐如 Web 会话、Web ⻚⾯命中计数)等。
spring-boot-starter-data-redis
Spring Boot
提供了对
Redis
集成的组件包:
spring-boot-starter-data-redis
,它依赖于
spring-data-redis
和
lettuce
。
Spring Boot 1.0
默认使⽤的是
Jedis
客户端,
2.0
替换成了
Lettuce
,但如果你从
Spring Boot 1.5.X
切换过来,⼏乎感受不⼤差异,这是因为
spring-boot-starter-data-redis
为我们隔离了其中的差异性。
- Lettuce:是⼀个可伸缩线程安全的 Redis 客户端,多个线程可以共享同⼀个 RedisConnection,它利⽤ 优秀 Netty NIO 框架来⾼效地管理多个连接。
- Spring Data:是 Spring 框架中的⼀个主要项⽬,⽬的是为了简化构建基于 Spring 框架应⽤的数据访 问,包括⾮关系数据库、Map-Reduce 框架、云数据服务等,另外也包含对关系数据库的访问⽀持。
- Spring Data Redis:是 Spring Data 项⽬中的⼀个主要模块,实现了对 Redis 客户端 API 的⾼度封装, 使对 Redis 的操作更加便捷。
可以⽤以下⽅式来表达它们之间的关系:
Lettuce ! Spring Data Redis ! Spring Data ! spring-boot-starter-data-redis
因此
Spring Data Redis
和
Lettuce
具备的功能,
spring-boot-starter-data-redis
⼏乎都会有。
快速上⼿
相关配置
引⼊依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
引⼊
commons-pool 2
是因为
Lettuce
需要使⽤
commons-pool 2
创建
Redis
连接池。
application 配置GitChat
# Redis 数据库索引(默认为 0)
spring.redis.database=0
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务器连接端⼝
spring.redis.port=6379
# Redis 服务器连接密码(默认为空)
spring.redis.password=
# 连接池最⼤连接数(使⽤负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最⼤阻塞等待时间(使⽤负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最⼤空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最⼩空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
从配置也可以看出
Spring Boot
默认⽀持
Lettuce
连接池。
缓存配置
在这⾥可以为
Redis
设置⼀些全局配置,⽐如配置主键的⽣产策略
KeyGenerator
,如不配置会默认使⽤参数
名作为主键。
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport{
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params)
{
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
}
注意,我们使⽤了注解:
@EnableCaching
来开启缓存。
GitChat
测试使⽤
在单元测试中,注⼊
RedisTemplate
。
String
是最常⽤的⼀种数据类型,普通的
key/value
存储都可以归为此
类,
value
其实不仅是
String
也可以是数字。
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRedisTemplate {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testString() {
redisTemplate.opsForValue().set("neo", "ityouknow");
Assert.assertEquals("ityouknow", redisTemplate.opsForValue().get("neo"));
}
}
在这个单元测试中,我们使⽤
redisTemplate
存储了⼀个字符串
"ityouknow"
,存储之后获取进⾏验证,
多次
进⾏
set
相同的
key
,键对应的值会被覆盖
。
从上⾯的整个流程来看,使⽤
spring-boot-starter-data-redis
只需要三步就可以快速地集成
Redis
进⾏操作,
下⾯介绍
Redis
如何操作各种数据类型。
各类型实践
我们知道
Redis
⽀持多种数据类型,实体、哈希、列表、集合、有序集合,那么在
Spring Boot
体系中都如
何使⽤呢?
实体
先来看
Redis
对
Pojo
的⽀持,新建⼀个
User
对象,放到缓存中,再取出来。
@Test
public void testObj(){
User user=new User("[email protected]", "smile", "youknow", "know","2020");
ValueOperations<String, User> operations=redisTemplate.opsForValue();
operations.set("com.neo", user);
User u=operations.get("com.neo");
System.out.println("user: "+u.toString());
}
输出结果:
user: com.neo.domain.User@16fb356[id=<null>,userName=know,passWord=youknow,email=i
[email protected],nickName=smile,regTime=2020]
验证发现完美⽀持对象的存⼊和读取。
超时失效
Redis
在存⼊每⼀个数据的时候都可以设置⼀个超时时间,过了这个时间就会⾃动删除数据,这种特性⾮常
适合我们对阶段数据的缓存。
新建⼀个
User
对象,存⼊
Redis
的同时设置
100
毫秒后失效,设置⼀个线程暂停
1000
毫秒之后,判断数
据是否存在并打印结果。
@Test
public void testExpire() throws InterruptedException {
User user=new User("[email protected]", "expire", "youknow", "expire","2020");
ValueOperations<String, User> operations=redisTemplate.opsForValue();
operations.set("expire", user,100,TimeUnit.MILLISECONDS);
Thread.sleep(1000);
boolean exists=redisTemplate.hasKey("expire");
if(exists){
System.out.println("exists is true");
}else{
System.out.println("exists is false");
}
}
输出结果:
exists is false
从结果可以看出,
Reids
中已经不存在
User
对象了,此数据已经过期,同时我们在这个测试的⽅法中使⽤了
hasKey("expire")
⽅法,可以判断
key
是否存在。
删除数据
有些时候,我们需要对过期的缓存进⾏删除,下⾯来测试此场景的使⽤。⾸
set
⼀个字符串
“ityouknow”
,紧
接着删除此
key
的值,再进⾏判断。
GitChat
@Test
public void testDelete() {
ValueOperations<String, User> operations=redisTemplate.opsForValue();
redisTemplate.opsForValue().set("deletekey", "ityouknow");
redisTemplate.delete("deletekey");
boolean exists=redisTemplate.hasKey("deletekey");
if(exists){
System.out.println("exists is true");
}else{
System.out.println("exists is false");
}
}
输出结果:
exists is false
结果表明字符串
“ityouknow”
已经被成功删除。
Hash(哈希)
⼀般我们存储⼀个键,很⾃然的就会使⽤
get/set
去存储,实际上这并不是很好的做法。
Redis
存储⼀个
key
会有⼀个最⼩内存,不管你存的这个键多⼩,都不会低于这个内存,因此合理的使⽤
Hash
可以帮我们节省
很多内存。
Hash Set
就在哈希表
Key
中的域(
Field
)的值设为
value
。如果
Key
不存在,⼀个新的哈希表被创建并进
⾏
HSET
操作;如果域(
fifield
)已经存在于哈希表中,旧值将被覆盖。
@Test
public void testHash() {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
hash.put("hash","you","you");
String value=(String) hash.get("hash","you");
System.out.println("hash value :"+value);
}
输出结果:
hash value :you
根据上⾯测试⽤例发现,
Hash set
的时候需要传⼊三个参数,第⼀个为
key
,第⼆个为
fifield
,第三个为存储
的值。⼀般情况下
Key
代表⼀组数据,
fifield
为
key
相关的属性,⽽
value
就是属性对应的值。
List
Redis List
的应⽤场景⾮常多,也是
Redis
最重要的数据结构之⼀。 使⽤
List
可以轻松的实现⼀个队列,
List
典型的应⽤场景就是消息队列,可以利⽤
List
的
Push
操作,将任务存在
List
中,然后⼯作线程再⽤
POP
操作将任务取出进⾏执⾏。
@Test
public void testList() {
ListOperations<String, String> list = redisTemplate.opsForList();
list.leftPush("list","it");
list.leftPush("list","you");
list.leftPush("list","know");
String value=(String)list.leftPop("list");
System.out.println("list value :"+value.toString());
}
输出结果:
list value :know
上⾯的例⼦我们从左侧插⼊⼀个
key
为
"list"
的队列,然后取出左侧最近的⼀条数据。其实
List
有很多
API
可以操作,⽐如从右侧进⾏插⼊队列从右侧进⾏读取,或者通过⽅法
range
读取队列的⼀部分。接着上⾯的
例⼦我们使⽤
range
来读取。
List<String> values=list.range("list",0,2);
for (String v:values){
System.out.println("list range :"+v);
}
输出结果:
list range :know
list range :you
list range :it
range
后⾯的两个参数就是插⼊数据的位置,输⼊不同的参数就可以取出队列中对应的数据。
Redis List
的实现为⼀个双向链表,即可以⽀持反向查找和遍历,更⽅便操作,不过带来了部分额外的
内存开销,
Redis
内部的很多实现,包括发送缓冲队列等也都是⽤的这个数据结构。
Set
Redis Set
对外提供的功能与
List
类似是⼀个列表的功能,特殊之处在于
Set
是可以⾃动排重的,当你需要
存储⼀个列表数据,⼜不希望出现重复数据时,
Set
是⼀个很好的选择,并且
Set
提供了判断某个成员是否
在⼀个
Set
集合内的重要接⼝,这个也是
List
所不能提供的。
@Test
public void testSet() {
String key="set";
SetOperations<String, String> set = redisTemplate.opsForSet();
set.add(key,"it");
set.add(key,"you");
set.add(key,"you");
set.add(key,"know");
Set<String> values=set.members(key);
for (String v:values){
System.out.println("set value :"+v);
}
}
输出结果:
set value :it
set value :know
set value :you
通过上⾯的例⼦我们发现,输⼊了两个相同的值
“you”
,全部读取的时候只剩下了⼀条,说明
Set
对队列进⾏
了⾃动的排重操作。
Redis
为集合提供了求交集、并集、差集等操作,可以⾮常⽅便的使⽤。
测试 difference
SetOperations<String, String> set = redisTemplate.opsForSet();
String key1="setMore1";
String key2="setMore2";
set.add(key1,"it");
set.add(key1,"you");
set.add(key1,"you");
set.add(key1,"know");
set.add(key2,"xx");
set.add(key2,"know");
Set<String> diffs=set.difference(key1,key2);
for (String v:diffs){
System.out.println("diffs set value :"+v);
}
输出结果:
diffs set value :it
diffs set value :you
根据上⾯这个例⼦可以看出,
difference()
函数会把
key 1
中不同于
key 2
的数据对⽐出来,这个特性适合我
们在⾦融场景中对账的时候使⽤。
测试 unions
SetOperations<String, String> set = redisTemplate.opsForSet();
String key3="setMore3";
String key4="setMore4";
set.add(key3,"it");
set.add(key3,"you");
set.add(key3,"xx");
set.add(key4,"aa");
set.add(key4,"bb");
set.add(key4,"know");
Set<String> unions=set.union(key3,key4);
for (String v:unions){
System.out.println("unions value :"+v);
}
输出结果:
unions value :know
unions value :you
unions value :xx
unions value :it
unions value :bb
unions value :aa
根据例⼦我们发现,
unions
会取两个集合的合集,
Set
还有其他很多类似的操作,⾮常⽅便我们对集合进⾏
数据处理。
Set
的内部实现是⼀个
Value
永远为
null
的
HashMap
,实际就是通过计算
Hash
的⽅式来快速排重,
这也是
Set
能提供判断⼀个成员是否在集合内的原因。
ZSet
Redis Sorted Set
的使⽤场景与
Set
类似,区别是
Set
不是⾃动有序的,⽽
Sorted Set
可以通过⽤户额外提
供⼀个优先级(
Score
)的参数来为成员排序,并且是插⼊有序,即⾃动排序。
在使⽤
Zset
的时候需要额外的输⼊⼀个参数
Score
,
Zset
会⾃动根据
Score
的值对集合进⾏排序,我们可
以利⽤这个特性来做具有权重的队列,⽐如普通消息的
Score
为
1
,重要消息的
Score
为
2
,然后⼯作线程
可以选择按
Score
的倒序来获取⼯作任务。
GitChat
@Test
public void testZset(){
String key="zset";
redisTemplate.delete(key);
ZSetOperations<String, String> zset = redisTemplate.opsForZSet();
zset.add(key,"it",1);
zset.add(key,"you",6);
zset.add(key,"know",4);
zset.add(key,"neo",3);
Set<String> zsets=zset.range(key,0,3);
for (String v:zsets){
System.out.println("zset value :"+v);
}
Set<String> zsetB=zset.rangeByScore(key,0,3);
for (String v:zsetB){
System.out.println("zsetB value :"+v);
}
}
输出结果:
zset value :it
zset value :neo
zset value :know
zset value :you
zsetB value :it
zsetB value :neo
通过上⾯的例⼦我们发现插⼊到
Zset
的数据会⾃动根据
Score
进⾏排序,根据这个特性我们可以做优先队
列等各种常⻅的场景。另外
Redis
还提供了
rangeByScore
这样的⼀个⽅法,可以只获取
Score
范围内排序
后的数据。
Redis Sorted Set
的内部使⽤
HashMap
和跳跃表(
SkipList
)来保证数据的存储和有序,
HashMap
⾥
放的是成员到
Score
的映射,⽽跳跃表⾥存放的是所有的成员,排序依据是
HashMap
⾥存的
Score
,
使⽤跳跃表的结构可以获得⽐较⾼的查找效率,并且在实现上⽐较简单。
封装
在我们实际的使⽤过程中,不会给每⼀个使⽤的类都注⼊
redisTemplate
来直接使⽤,⼀般都会对业务进⾏
简单的包装,最后提供出来对外使⽤。
我们举两个例⼦说明。
⾸先定义⼀个
RedisService
服务,将
RedisTemplate
注⼊到类中。
@Service
public class RedisService {
@Autowired
private RedisTemplate redisTemplate;
}
封装简单插⼊操作:
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForVal
ue();
operations.set(key, value);
result = true;
} catch (Exception e) {
logger.error("set error: key {}, value {}",key,value,e);
}
return result;
}
会对其中出现的异常继续处理,反馈给调⽤⽅。
⽐如我们想删除某⼀类的
Key
的值。
public void removePattern(final String pattern) {
Set<Serializable> keys = redisTemplate.keys(pattern);
if (keys.size() > 0)
redisTemplate.delete(keys);
}
使⽤
Redis
的
Pattern
来匹配出⼀批符合条件的缓存,然后批量进⾏删除。
还有其他封装⽅法,⽐如删除的时候先判断
Key
是否存在等,这些简单的业务判断都应该封装在
RedisService
,对外提供最简单的
API
调⽤即可。
@Autowired
private RedisService redisService;
@Test
public void testString() throws Exception {
redisService.set("neo", "ityouknow");
Assert.assertEquals("ityouknow", redisService.get("neo"));
}
在其他服务使⽤的时候将
RedisService
注⼊其中,调⽤对应的⽅法来操作
Redis
,这样会更优雅简单⼀些。
GitChat
总结
Redis
是⼀款⾮常优秀的⾼性能缓存中间件,被⼴泛的使⽤在各互联⽹公司中,
Spring Boot
对
Redis
的操作
提供了很多⽀持,可以⾮常⽅便的去集成。
Redis
拥有丰富的数据类型,⽅便我们在不同的业务场景中去使
⽤,特别是提供了很多内置的⾼效集合操作,在业务中使⽤⾮常⽅便。