Redis基本介绍及封装使用

  1. 1. 前言
    1. 1.1 Redis简介
    2. 1.2 Redis的数据类型
    3. 1.3 Redis适用场景
  2. 2. 搭建Redis服务
    1. 2.1 准备Docker环境
    2. 2.2 搭建Redis服务
    3. 2.3 Redis数据库的可视化连接
  3. 3. 在Springboot整合Redis的封装
    1. 3.1 依赖及配置文件
    2. 3.2 整合Redis的代码封装
  4. 4. Python操作Redis缓存数据
    1. 4.1 普通方式存取数据
    2. 4.2 hash方式存取数据
  5. 5. 参考资料

1. 前言

1.1 Redis简介

Redis 是完全开源免费的,遵守 BSD 协议,是一个灵活的高性能 key-value 数据结构存储,可以用来作为数据库、缓存和消息队列。

Redis 比其他 key-value 缓存产品有以下三个特点:

  • Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载到内存使用。
  • Redis 不仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
  • Redis 支持主从复制,即 master-slave 模式的数据备份。

Redis官方文档:http://redis.io/documentation

1.2 Redis的数据类型

Redis 支持 5 中数据类型:string(字符串),hash(哈希),list(列表),set(集合),zset(有序集合)。

  • string 是 redis 最基本的数据类型。一个 key 对应一个 value。string 是二进制安全的。也就是说 redis 的 string 可以包含任何数据。比如 jpg 图片或者序列化的对象。string 类型是 redis 最基本的数据类型,string 类型的值最大能存储 512 MB。
  • hash 是一个键值对(key - value)集合。Redis hash 是一个 string 类型的 key 和 value 的映射表,hash 特别适合用于存储对象。并且可以像数据库中一样只对某一项属性值进行存储、读取、修改等操作。
  • list 是简单的字符串列表,按照插入顺序排序。我们可以网列表的左边或者右边添加元素。list 就是一个简单的字符串集合,和 Java 中的 list 相差不大,区别就是这里的 list 存放的是字符串。list 内的元素是可重复的。可以做消息队列或最新消息排行等功能。
  • set 是字符串类型的无序集合。集合是通过哈希表实现的,因此添加、删除、查找的复杂度都是 O(1)。redis 的 set 是一个 key 对应着多个字符串类型的 value,也是一个字符串类型的集合,和 redis 的 list 不同的是 set 中的字符串集合元素不能重复,但是 list 可以,也就是具有唯一性。
  • zset 和 set 一样都是字符串类型元素的集合,并且集合内的元素不能重复。不同的是 zset 每个元素都会关联一个 double 类型的分数。redis 通过分数来为集合中的成员进行从小到大的排序。zset 的元素是唯一的,但是分数(score)却可以重复。可用作排行榜等场景。

1.3 Redis适用场景

缓存:缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。

排行榜:很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。

计数器:为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。

分布式会话:集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。

分布式锁:在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。

社交网络:点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。

最新列表:Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。

消息系统:消息队列是大型网站必用的中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统,但是这个不能和专业的消息中间件相比。

2. 搭建Redis服务

以下我将采用Docker的方式进行搭建,VPS系统用的是Debian 11 x86_64。VPS的购买及配置、Docker的概念及使用…这些基本的就不再赘述了,如果不会的话见我的另一篇博客:VPS基本部署环境的搭建与配置

2.1 准备Docker环境

1
2
3
4
$ apt-get update -y && apt-get install curl -y  # 安装curl
$ curl https://get.docker.com | sh - # 安装docker
$ sudo systemctl start docker # 启动docker服务
$ docker version # 查看docker版本(客户端要与服务端一致)

2.2 搭建Redis服务

1
2
3
$ docker pull redis:3.2.8
$ docker run --name redis -p 6379:6379 -d redis:3.2.8 --requirepass "mypassword"
$ docker update redis --restart=always

2.3 Redis数据库的可视化连接

建议使用 AnotherRedisDesktopManager 开源工具进行可视化连接和管理。

ARDM工具

3. 在Springboot整合Redis的封装

3.1 依赖及配置文件

pom.xml的依赖:

1
2
3
4
5
6
7
8
<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>

application.properties配置文件:

1
2
3
4
5
6
7
8
9
## redis config
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.password=redisPassword
spring.redis.lettuce.pool.max-active=-1
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.max-idle=200
spring.redis.lettuce.pool.min-idle=20

3.2 整合Redis的代码封装

代码封装的目录结构如下:

1
2
3
4
5
6
7
8
├── annotation
│   ├── RedisEntity.java
│   └── RedisKey.java
├── config
│   └── RedisConfig.java
└── service
├── RedisService.java
└── SampleRedisService.java

RedisEntity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisEntity {

//用于生成key的key模板,例:TBoxData_%s_%s,每个占位符(%s)用带有@RedisKey注解的字段的值填充
String keyTemplate() default "";

long expire() default 0;//过期时间,单位:秒

}

RedisKey.java

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisKey {
int order() default 9999;//key序号,用于区分字段值在key中的位置,越小越靠前
}

RedisConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 配置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
//指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key采用String的序列化方式
redisTemplate.setKeySerializer(stringRedisSerializer);
//hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);
//value序列化方式采用jackson
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//hash的value序列化方式采用jackson
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}

RedisService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import com.yoyo.admin.redis_common.annotation.RedisEntity;
import com.yoyo.admin.redis_common.annotation.RedisKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
public class RedisService {

private RedisTemplate<String, Object> redisTemplate;

@Autowired
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}

//获取实体类型Key模板
public String getEntityTemplate(Class<?> typeClass) {
RedisEntity redisEntity = typeClass.getAnnotation(RedisEntity.class);
if (redisEntity == null) {
return null;
}
if (!redisEntity.keyTemplate().isEmpty()) {
return redisEntity.keyTemplate();
} else {
return typeClass.getName();
}
}

//从实体定义中获取key
private <T> String getEntityKey(T t) throws IllegalAccessException {
Class<?> typeClass = t.getClass();
RedisEntity redisEntity = typeClass.getAnnotation(RedisEntity.class);
if (redisEntity == null) {
return null;
}
if (!redisEntity.keyTemplate().isEmpty()) {
String template = redisEntity.keyTemplate();
LinkedList<Integer> orders = new LinkedList<>();
LinkedList<String> keyParameters = new LinkedList<>();
Field[] fields = typeClass.getDeclaredFields();
if (fields.length > 0) {
for (Field field : fields) {
field.setAccessible(true);
RedisKey redisKey = field.getAnnotation(RedisKey.class);
if (redisKey != null) {
int order = redisKey.order();
boolean inserted = false;
for (int i = 0; i <= orders.size() - 1; i++) {
if (order < orders.get(i)) {
orders.add(i, order);
keyParameters.add(i, field.get(t) != null ? field.get(t).toString() : "");
inserted = true;
break;
}
}
if (!inserted) {
orders.addLast(order);
keyParameters.addLast(field.get(t) != null ? field.get(t).toString() : "");
}
}
}
}
if (keyParameters.size() > 0) {
return String.format(template, keyParameters.toArray());
} else {
return template;
}
} else {
return typeClass.getName();
}
}

//从实体定义中获取过期时间
private <T> Long getEntityExpire(T t) {
Class<?> typeClass = t.getClass();
RedisEntity redisEntity = typeClass.getAnnotation(RedisEntity.class);
if (redisEntity == null || redisEntity.expire() <= 0) {
return null;
}
return redisEntity.expire();
}

//向redis写入值,使用实体定义中的key
public String set(Object t) throws IllegalAccessException, RuntimeException {
String key = getEntityKey(t);
if (key == null) {
throw new RuntimeException("Redis Set : Type has no @RedisEntity annotation");
}
Long expire = getEntityExpire(t);
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, t);
if (expire != null) {
expire(key, expire);
}
return key;
}

//向redis写入值,使用实体定义中的key
public String set(String key,Object t) throws IllegalAccessException, RuntimeException {
if (key == null) {
throw new RuntimeException("Redis Set : Type has no @RedisEntity annotation");
}
Long expire = getEntityExpire(t);
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, t);
if (expire != null) {
expire(key, expire);
}
return key;
}

//从redis中按照实体定义使用指定参数生成key以得到value
public <T> T get(Class<T> tClass, Object... keyParams) throws RuntimeException {
RedisEntity redisEntity = tClass.getAnnotation(RedisEntity.class);
if (redisEntity == null) {
throw new RuntimeException("Redis Get : Type has no @RedisEntity annotation");
}
String template = redisEntity.keyTemplate();
if (keyParams != null && keyParams.length > 0) {
String key = String.format(template, keyParams);
return getForType(tClass, key);
} else {
return getForType(tClass, template);
}
}

//从redis中按照实体定义使用指定参数生成key以得到value
public <T> T get(Class<T> tClass, String key) throws RuntimeException {
return getForType(tClass, key);
}

//从redis中使用指定key以得到特定类型的value
public <T> T getForType(Class<T> type, String key) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
Object object = operations.get(key);
if (object != null && type.equals(object.getClass())) {
return type.cast(object);
}
return null;
}

//从redis中使用指定key集合以得到特定类型的value集合
public <T> List<T> listForType(Class<T> type, List<String> keys) {
List<T> list = new ArrayList<>();
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
List<Object> objects = operations.multiGet(keys);
if (objects == null || objects.size() == 0) {
return list;
}
for (int i = 0; i <= objects.size() - 1; i++) {
Object object = objects.get(i);
if (object != null && type.equals(object.getClass())) {
list.add(type.cast(object));
}
}
return list;
}

//使用实体设置的模板获取实体所有的key
public <T> Set<String> keys(Class<T> tClass, Object... keyParams) throws RuntimeException {
RedisEntity redisEntity = tClass.getAnnotation(RedisEntity.class);
if (redisEntity == null) {
throw new RuntimeException("Redis Keys(Class<T>,Object...) : Type has no @RedisEntity annotation");
}
String template = redisEntity.keyTemplate();
String pattern = String.format(template, keyParams);
return keys(pattern);
}

//使用实体设置的模板获取实体所有的key
public <T> Set<String> keys(Class<T> tClass) throws RuntimeException {
RedisEntity redisEntity = tClass.getAnnotation(RedisEntity.class);
if (redisEntity == null) {
throw new RuntimeException("Redis Keys(Class<T>) : Type has no @RedisEntity annotation");
}
String template = redisEntity.keyTemplate();
String pattern = template.substring(0, template.indexOf("%"));
return keys(pattern + "*");
}

//获取所有匹配的key
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}

//使用实体设备的模板删除实体
public <T> void delete(Class<T> tClass, Object... keyParams) throws RuntimeException {
RedisEntity redisEntity = tClass.getAnnotation(RedisEntity.class);
if (redisEntity == null) {
throw new RuntimeException("Redis Delete : Type has no @RedisEntity annotation");
}
String template = redisEntity.keyTemplate();
String key = String.format(template, keyParams);
delete(key);
}

//删除key
public void delete(String key) {
redisTemplate.delete(key);
}

//批量删除key
public void delete(Collection<String> keys) {
redisTemplate.delete(keys);
}

//为指定Key设置过期时间,单位秒
public void expire(String key, long expire) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}

//判断key是否存在
public <T> Boolean hasKey(Class<T> tClass, Object... keyParams) {
RedisEntity redisEntity = tClass.getAnnotation(RedisEntity.class);
if (redisEntity == null) {
return false;
}
String template = redisEntity.keyTemplate();
return hasKey(String.format(template, keyParams));
}

//判断key是否存在
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}

}

SampleRedisService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class SampleRedisService {

private RedisTemplate<String, Object> redisTemplate;

@Autowired
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}

public void setString(String key, String str) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, str);
}

public String getString(String key) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
Object object = operations.get(key);
if (object instanceof String) {
return (String) object;
}
return null;
}

public void setMap(String key, Map<?, ?> map) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, map);
}

public Map<?, ?> getMap(String key) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
Object object = operations.get(key);
if (object instanceof Map) {
return (Map<?, ?>) object;
}
return null;
}

public void setList(String key, List<?> list) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, list);
}

public List<?> getList(String key) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
Object object = operations.get(key);
if (object instanceof List) {
return (List<?>) object;
}
return null;
}
}

4. Python操作Redis缓存数据

4.1 普通方式存取数据

Step1:引入redis库

1
$ pip install redis

Step2:使用Redis

往redis存值

1
2
3
4
5
import redis

pool = redis.ConnectionPool(host='127.0.0.1', port=6379, password='123456', db=0)
r = redis.Redis(connection_pool=pool)
r.set('id', '666666')

从redis取值

1
2
3
4
5
import redis

pool = redis.ConnectionPool(host='127.0.0.1', port=6379, password='123456', db=0)
r = redis.Redis(connection_pool=pool)
get_value = r.get('id')

注意事项:如果存入字典,最新版的会出现如下报错

1
redis.exceptions.DataError: Invalid input of type: 'dict'. Convert to a bytes, string, int or float first.

可以降级版本解决该问题

1
$ pip install redis==2.10.6

4.2 hash方式存取数据

单个方式的存取值:hset设置单个值、hgetall获取整个hash、hget根据key获取单独的value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding: utf-8 -*-

import redis

pool = redis.ConnectionPool(host="127.0.0.1", port=6379, password="123456", db=1, decode_responses=True)
r = redis.Redis(connection_pool=pool)

test_list = [1, 2, 3, "123", [1, 2, 3, "123"]]
dict_list = {"test_list": [1, 2, 3, "123", [1, 2, 3, "123"]], "test_list2": [1, 2, 3]}
for dl in dict_list:
r.hset("dl_hash", dl, dict_list[dl])
# 设置过期时间,单位秒
r.expire("dl_hash", 6000)

# 获取整个hash
print(r.hgetall("dl_hash"))
# 根据key获取单独的value
print(r.hget("dl_hash", "test_list2"))

>>> 结果输出
{b'test_list': b"[1, 2, 3, '123', [1, 2, 3, '123']]", b'test_list2': b'[1, 2, 3]'}
b'[1, 2, 3]'

注:设置 decode_responses=True 是为了解决Redis与Python交互取出来的是bytes类型的问题。

Python使用Hash方式存储Redis

批量方式的存取值:hmset 批量设置多个值,hmget 批量获取多个值

1
2
r.hmset('xx', {'k1':'v1', 'k2': 'v2'})
r.hmget('xx', 'k1', 'k2')

注意:redis库不能操作 redis 集群,实际使用中会遇到如下错误,可以使用 redis-py-cluster 库去实现 redis 集群的操作。

1
redis.exceptions.ResponseError: MOVED 12285 192.168.222.66:6384

5. 参考资料

[1] Redis教程 - Redis知识体系详解 from Java全栈知识体系

[2] Redis基本原理 from 知乎

[3] Redis原理和机制详解 from 知乎

[4] Redis架构原理及应用实践 from segmentfault

[5] Redis 的 8 大应用场景 from segmentfault

[6] Linux下使用docker部署Redis from CSDN