本文内容来自Spring Boot相关书籍学习总结
Redis是目前使用广泛的缓存服务,与Memcached等缓存中间件相比,Redis支持数据持久化,支持更多的数据结构和更丰富的数据操作,此外,Redis有着高可用的集群方案和广泛的应用场景。
1、Redis入门
1.1 Redis简介
Redis的主要特点如下:
1)支持数据的持久化,可以将内存中的数据持久化保存在磁盘中,重启后再次将磁盘中的数据加载到内存。
2)丰富的数据类型,不仅支持简单的key-value类型的数据,还提供List、Set、ZSet、Hash等数据结构的存储。
3)支持数据的备份,即master-slave(主-从)模式的数据备份。
4)丰富的特性,支持publish/subscribe(发布/订阅)、通知、key过期等特性。
1.2 Redis数据类型
Redis中主要有5种数据类型:String(字符串类型)、Hash(哈希类型)、List(列表类型)、Set(集合类型)、ZSet(有序集合类型)。
| 数据类型 | 说明 | 主要方法 | 适用场景 |
|---|---|---|---|
| String | String类型是Redis中最简单、最常用的数据结构,既可以存储文字(例如"hello world"),又可以存储数字(例如整数10086和浮点数3.14),还可以存储二进制数据(例如10010100)。 | set、get、decr、incr、mget | 主键生成、数据缓存 |
| Hash | Hash(哈希表)是一个String类型的Field(字段)和Value(值)之间的映射表,类似于Java中的HashMap。一个哈希表由多个字段-值对(Field-Value Pair)组成,值可以是文字、整数、浮点数或者二进制数据。在同一个哈希表中,每个字段的名称必须是唯一的。 | hset、hget、hmget | 缓存用户信息 |
| List | List(列表)类型是基于双向链表实现的,可以支持正向、反向查找和遍历。从用户角度来说,List是简单的字符串列表,字符串按照添加的顺序排序。可以添加一个元素到List列表的头部(左边)或者尾部(右边)。一个List最多可以包含2^32-1个元素(最多可超过40亿个元素)。 | lpush、rpush、lpop、rpop、llen | 标签、收藏、消息队列等 |
| Set | Set(集合)是String类型的无序集合,其实也是通过哈希表实现的。集合元素是惟一的,它会自动去掉重复元素。 | sadd、scard、sismember、srem | 共同好友等 |
| ZSet | ZSet和Set一样也是String类型元素的集合,并且不允许出现重复的成员。区别在于:ZSet每个元素会关联一个double类型的分数Score,它会根据提供的score参数自动排序。 | zadd、zcard、zrange | 排行榜、关注度 |
2、 Spring Boot集成Redis
2.1 Spring Boot对Redis的支持
Spring Boot提供了集成Redis的组件包spring-boot-starter-data-redis,能够非常方便地集成到项目中。Spring Boot 2.0版本之后默认使用Lettuce客户端。
<!--引入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>
<version>2.8.0</version>
</dependency>
添加配置文件
# 配置redis
#Redis数据库(默认0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.85.150
# Redis服务器连接端口
spring.redis.port=6379
# 连接超时时间(毫秒)
spring.redis.timeout=60s
# lettuce连接池配置
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=1000
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=10
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=2
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
Jedis和Lettuce的区别:
Jedis在实现上直连Redis服务器,在多线程环境下是非线程安全的,除非使用连接池为每个Jedis实例增加物理连接。
Lettuce基于Netty的连接实例(StatefulRedisConnection)可以在多个线程间并发访问,并且是线程安全的,它支持多线程环境下的并发访问,同时也是可伸缩的设计,在一个连接实例不够的情况下可以按需增加连接实例。
2.2 RedisTemplate
RedisTemplate是Spring针对Redis封装的一个比较强大的模板,以方便使用。只要在所需的地方注入RedisTemplate即可,无须其他额外配置,开箱即用。
RedisTemplate有两个方法经常用到:opsForXXX()和boundXXXOps(),XXX是value(值)的数据类型。opsForXXX获取到一个操作(Operation),但是没有指定操作的key(键),可以在一个连接(事务)内操作多个key以及对应的value;boundXXXOps获取到一个指定key的操作,在一个连接内只操作这个key对应的value。
如果不想用默认的序列化器,可以在Spring Boot的配置类中自定义RedisTemplate,修改RedisTemplate的序列化器。
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> initRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
//创建RedisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//key和hashkey使用string序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
//value和hashValue采用JSON序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
Spring提供了一个StringRedisTemplate类,它的key和value的序列化方式默认是String方式。
@Bean(name = "stringRedisTemplate")
public StringRedisTemplate initStringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
return stringRedisTemplate;
}
3、操作Redis数据结构
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
/**
* 字符串类型
*
* @throws JsonProcessingException
*/
@Test
public void one() throws JsonProcessingException {
Person employee = new Person(10001, 27, "mimangkai", "迷茫凯", "shanghai");
//获取字符串操作接口
ValueOperations valueOperations = redisTemplate.opsForValue();
final String key = "redis:test:1";
final String content = objectMapper.writeValueAsString(employee);
log.info("写入缓存中的用户实体对象信息为:{}", employee);
valueOperations.set(key, content);
//从缓存中读取内容
Object result = valueOperations.get(key);
if (result != null) {
Person resultP = objectMapper.readValue(result.toString(), Person.class);
log.info("读取缓存内容并反序列化后的结果:{}", resultP);
}
}
/**
* 列表类型
*/
@Test
public void two() {
List<Person> list = Lists.newArrayList();
list.add(new Person(10001, 27, "mimangkai", "迷茫凯", "shanghai"));
list.add(new Person(10002, 28, "mimangfei", "迷茫菲", "hangzhou"));
log.info("构造已经排好序的用户对象列表:{}", list);
final String key = "redis:test:2";
//获取列表操作接口
ListOperations listOperations = redisTemplate.opsForList();
list.forEach(e -> listOperations.leftPush(key, e));
log.info("--获取Redis中List的数据 - 从队头中获取--");
Object res = listOperations.rightPop(key);
Person employee;
while (res != null) {
employee = (Person) res;
log.info("当前数据:{}", employee);
res = listOperations.rightPop(key);
}
}
/**
* 集合类型
*/
@Test
public void three() {
List<String> userList = Lists.newArrayList();
userList.add("mimangkai");
userList.add("bigdata");
userList.add("wangkai");
userList.add("spark");
userList.add("bigdata");
log.info("待处理的用户姓名列表:{}", userList);
final String key = "redis:test:3";
//获取集合操作接口
SetOperations setOperations = redisTemplate.opsForSet();
userList.forEach(e -> setOperations.add(key, e));
//从缓存中获取用户对象集合
Object res = setOperations.pop(key);
while (res != null) {
log.info("当前用户:{}", res);
res = setOperations.pop(key);
}
}
/**
* 有序集合
*/
@Test
public void four() {
List<PhoneUser> list = Lists.newArrayList();
list.add(new PhoneUser("101", 120.0));
list.add(new PhoneUser("104", 100.0));
list.add(new PhoneUser("103", 125.0));
list.add(new PhoneUser("102", 130.0));
list.add(new PhoneUser("105", 70.0));
list.add(new PhoneUser("106", 148.0));
log.info("无序的用户手机充值列表:{}", list);
final String key = "redis:test:4";
//ZSet在add元素进入缓存后,下次不能进行更新,需要先清空缓存(实际生产环境中不建议这么使用)
redisTemplate.delete(key);
//获取有序集合操作接口
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
list.forEach(e -> zSetOperations.add(key, e, e.getFare()));
//获取充值排名靠前的用户列表
Long size = zSetOperations.size(key);
//从小到大排序
Set<PhoneUser> resSet = zSetOperations.range(key, 0L, size);
//从大到小排序
// Set<PhoneUser> resSet = zSetOperations.reverseRange(key, 0L, size);
resSet.forEach(e -> log.info("当前记录:{}", e));
}
/**
* 哈希存储
*/
@Test
public void five() {
List<Student> students = Lists.newArrayList();
List<Fruit> fruits = Lists.newArrayList();
students.add(new Student(10010, "mimangkai", "迷茫凯"));
students.add(new Student(10011, "mimangfei", "迷茫菲"));
students.add(new Student(10012, "mimangwei", "迷茫尉"));
fruits.add(new Fruit("apple", "红色"));
fruits.add(new Fruit("orange", "橙色"));
fruits.add(new Fruit("banana", "黄色"));
final String sKey = "redis:test:5";
final String fKey = "redis:test:6";
//获取哈希存储操作接口
HashOperations hashOperations = redisTemplate.opsForHash();
students.forEach(s -> hashOperations.put(sKey, s.getId(), s));
fruits.forEach(f -> hashOperations.put(fKey, f.getName(), f));
//获取对象列表
Map<String, Student> sMap = hashOperations.entries(sKey);
log.info("获取学生对象列表:{}", sMap);
Map<String, Student> fMap = hashOperations.entries(fKey);
log.info("获取水果对象列表:{}", fMap);
String sField = "10012";
Student student = (Student) hashOperations.get(sKey, sField);
log.info("获取指定的学生对象:{}->{}", sField, student);
String fField = "banana";
Fruit fruit = (Fruit) hashOperations.get(fKey, fField);
log.info("获取指定的水果对象:{}->{}", fField, fruit);
}
/**
* key过期失效
* @throws InterruptedException
*/
@Test
public void six() throws InterruptedException {
final String key1 = "redis:test:7";
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set(key1, "expire操作", 10L, TimeUnit.SECONDS);
Thread.sleep(5000L);
Boolean existKey1 = redisTemplate.hasKey(key1);
Object value = valueOperations.get(key1);
log.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey1, value);
Thread.sleep(5000L);
existKey1 = redisTemplate.hasKey(key1);
value = valueOperations.get(key1);
log.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey1, value);
}
@Test
public void seven() throws InterruptedException {
final String key2 = "redis:test:8";
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set(key2, "expire操作-2");
redisTemplate.expire(key2, 10L, TimeUnit.SECONDS);
Thread.sleep(5000L);
Boolean existKey2 = redisTemplate.hasKey(key2);
Object value = valueOperations.get(key2);
log.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey2, value);
Thread.sleep(5000L);
existKey2 = redisTemplate.hasKey(key2);
value = valueOperations.get(key2);
log.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey2, value);
}
4、工具类封装
/**
* redis工具类
*/
@Component
public class RedisOperator {
@Autowired
private StringRedisTemplate redisTemplate;
// Key(键),简单的key-value操作
/**
* 判断key是否存在
*
* @param key
* @return
*/
public boolean keyIsExist(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
*
* @param key
* @return
*/
public long ttl(String key) {
return redisTemplate.getExpire(key);
}
/**
* 实现命令:expire 设置过期时间,单位秒
*
* @param key
* @return
*/
public void expire(String key, long timeout) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:increment key,增加key一次
*
* @param key
* @return
*/
public long increment(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 实现命令:decrement key,减少key一次
*
* @param key
* @return
*/
public long decrement(String key, long delta) {
return redisTemplate.opsForValue().decrement(key, delta);
}
/**
* 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
*/
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 实现命令:DEL key,删除一个key
*
* @param key
*/
public void del(String key) {
redisTemplate.delete(key);
}
// String(字符串)
/**
* 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
*
* @param key
* @param value
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
*
* @param key
* @param value
* @param timeout (以秒为单位)
*/
public void set(String key, String value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 如果key不存在,则设置,如果存在,则报错
*
* @param key
* @param value
*/
public void setnx60s(String key, String value) {
redisTemplate.opsForValue().setIfAbsent(key, value, 60, TimeUnit.SECONDS);
}
/**
* 如果key不存在,则设置,如果存在,则报错
*
* @param key
* @param value
*/
public void setnx(String key, String value) {
redisTemplate.opsForValue().setIfAbsent(key, value);
}
/**
* 实现命令:GET key,返回 key所关联的字符串值。
*
* @param key
* @return value
*/
public String get(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
/**
* 批量查询,对应mget
*
* @param keys
* @return
*/
public List<String> mget(List<String> keys) {
return redisTemplate.opsForValue().multiGet(keys);
}
/**
* 批量查询,管道pipeline
*
* @param keys
* @return
*/
public List<Object> batchGet(List<String> keys) {
// nginx -> keepalive
// redis -> pipeline
List<Object> result = redisTemplate.executePipelined(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisConnection src = (StringRedisConnection) connection;
for (String k : keys) {
src.get(k);
}
return null;
}
});
return result;
}
// Hash(哈希表)
/**
* 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
*
* @param key
* @param field
* @param value
*/
public void hset(String key, String field, Object value) {
redisTemplate.opsForHash().put(key, field, value);
}
/**
* 实现命令:HGET key field,返回哈希表 key中给定域 field的值
*
* @param key
* @param field
* @return
*/
public String hget(String key, String field) {
return (String) redisTemplate.opsForHash().get(key, field);
}
/**
* 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
*
* @param key
* @param fields
*/
public void hdel(String key, Object... fields) {
redisTemplate.opsForHash().delete(key, fields);
}
/**
* 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
*
* @param key
* @return
*/
public Map<Object, Object> hgetall(String key) {
return redisTemplate.opsForHash().entries(key);
}
// List(列表)
/**
* 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
*
* @param key
* @param value
* @return 执行 LPUSH命令后,列表的长度。
*/
public long lpush(String key, String value) {
return redisTemplate.opsForList().leftPush(key, value);
}
/**
* 实现命令:LPOP key,移除并返回列表 key的头元素。
*
* @param key
* @return 列表key的头元素。
*/
public String lpop(String key) {
return (String) redisTemplate.opsForList().leftPop(key);
}
/**
* 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
*
* @param key
* @param value
* @return 执行 LPUSH命令后,列表的长度。
*/
public long rpush(String key, String value) {
return redisTemplate.opsForList().rightPush(key, value);
}
}