Spring Boot Redis 入门、详解、概念、实战
-
Redis介绍
- Redis 是一个速度非常快的非关系数据库(non-relational database),它可以存储键(key)与 5 种不同类型的值(value)之间的映射(mapping)可以将存储在内存的键值对数据持久化到硬盘。
- 可以使用复制特性来扩展读性能,还可以使用客户端分片来扩展写性能。
- 为了满足高性能,Redis 采用内存(in-memory)数据集(dataset)。根据使用场景,可以通过每隔一段时间转储数据集到磁盘,或者追加每条命令到日志来持久化。如果只需要一个功能丰富,网络化的内存缓存,持久化也可以被禁用。
-
Redis数据模型
- 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 页面命中计数)等。
-
新建项目之前 : Win 10 环境下安装 Redis
-
第一种方法:访问 https://start.spring.io/ , 点击下方Generate the Project,生成压缩包之后用 IDEA 打开
-
第二种方法新建 Spring Boot Redis项目:
-
选择Spring Initializer初始化项目
-
项目名为redisdemo,包名为wen
-
勾选web选项
-
勾选NoSQL – redis 选项,NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库
-
点击finish,构建工程
-
项目结构
-
pom.xml
- Spring Boot 提供了对 Redis 集成的组件包: spring-boot-starter-data-redis ,spring-boot-starter-data-redis 依赖于 spring-data-redis 和 jedis
- Jedis:Jedis 是 Redis 的 Java 版客户端实现,也是官方推荐的 Java 版客户端。它封装了对 Redis 的各种操作,并且支持事务、管道及有 Jedis 自身实现的分布式
- Spring Data:Spring Data 是 Spring 框架中的一个主要项目,目的是为了简化构建基于Spring 框架应用的数据访问,包括非关系数据库、Map-Reduce 框架、云数据服务等,另外也包含对关系数据库的访问支持
- Spring Data Redis :Spring Data Redis 是 Spring Data 项目中的一个主要模块,实现了对 Jedis 客户端 API 的高度封装,使对 Redis 的操作更加便捷
- 关系:Jedis – Spring Data Redis – Spring Data – spring-boot-starter-data-redis
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.8.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>wen</groupId> <artifactId>redisdemo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>redisdemo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
application.properties:
- Spring Boot 默认继承了连接池
spring.redis.database=1 spring.redis.host=127.0.0.1 spring.redis.port=6379 # redis 连接池设置 spring.redis.jedis.pool.max-idle=8 spring.redis.jedis.pool.min-idle=0 spring.redis.jedis.pool.max-active=8 spring.redis.jedis.pool.max-wait=-1 spring.redis.timeout=60000
-
logback.xml 配置文件
<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d %p (%file:%line\)- %m%n</pattern> <charset>GBK</charset> </encoder> </appender> <appender name="baselog" class="ch.qos.logback.core.rolling.RollingFileAppender"> <File>log/base.log</File> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>log/base.log.%d.%i</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>64 MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder> <pattern> %d %p (%file:%line\)- %m%n </pattern> <charset>UTF-8</charset> </encoder> </appender> <root level="info"> <appender-ref ref="STDOUT"/> </root> <logger name="com.example" level="DEBUG"> <appender-ref ref="baselog"/> </logger> </configuration>
-
TestController.java 测试连接
- 在测试testVar方法时,网址输入“http://localhost:8080/test/Stephanie” 代表传给controller类的参数是Stephanie,所以返回了网址给定的变量名
package wen.redisdemo.web; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { protected static Logger logger = LoggerFactory.getLogger(TestController.class); @RequestMapping("/") public String test() { logger.debug("测试连接"); return "连接成功!"; } @RequestMapping("/test/{var}") public String testVar(@PathVariable String var) { logger.debug("var={}", var); return "test " + var; } }
-
开启项目后(RedisdemoApplication)测试结果
-
接下来测试使用RedisTemplate, 由于Redis本质是一种缓存,所以它并不需要被写入数据库
-
实体类 User.java
package wen.redisdemo.entity; import java.io.Serializable; public class User implements Serializable { private Long id; private String username; private String password; public User() { super(); } public User(Long id, String username, String password) { this.id = id; this.username = username; this.password = password; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String toString() { return "username: " + this.getUsername() + "\n" + "password: " + this.getPassword(); } }
-
接口类 UserMapper.java
- 在接口中注入RedisTemplate:RedisTemplate是一个储存键值类型数据的接口,但是具体实现储存的形式主要有五种,opsForValue、opsForList、opsForSet、opsForHash、opsForZSet
- 如下文的 opsForValue(),这个方法会返回一个默认的操作类 DefaultValueOperations, 它提供了所有Redis字符串类型的操作api。比如set,get,incr等等。使用这些方法可以直接存储任意的java类型,而不需要将存储的东西序列化以及反序列化
- ListOperations, SetOperations, ZSetOperations除了提供的操作API不一样以外,其他的调用方法与DefaultValueOperations一致
package wen.redisdemo.mapper; 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.Repository; import wen.redisdemo.entity.User; @Repository public class UserMapper { @Autowired RedisTemplate redisTemplate; public void saveUser(User user) { ValueOperations<Long, User> valueOperations = redisTemplate.opsForValue(); valueOperations.set(user.getId(), user); System.out.println(valueOperations.get("1")); } public User getUser(Long id) { ValueOperations<Long, User> valueOperations = redisTemplate.opsForValue(); return valueOperations.get(id); } }
-
RedisController.java
- /setUser :浏览器链接中输入user属性传给RedisController,之后调用UserMapper中的saveUser利用上文中的ValueOperations进行缓存存储
- /getUser :参数只有id,即根据id来查询user信息,调用方法和上一点一样
package wen.redisdemo.web; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import wen.redisdemo.entity.User; import wen.redisdemo.mapper.UserMapper; @RestController public class RedisController { protected static Logger logger = LoggerFactory.getLogger(RedisController.class); @Autowired UserMapper userMapper; @RequestMapping("/setUser") public void setUser(Long id, String username, String password) { logger.debug("setUser:id={}, username={}, password={}", id, username, password); User user = new User(id, username, password); userMapper.saveUser(user); } @RequestMapping("/getUser") public String getUser(Long id) { return "username:" + userMapper.getUser(id).getUsername() + "\n" + "password: " + userMapper.getUser(id).getPassword(); } }
-
浏览器输入:http://localhost:8080/setUser?id=1&&username=Stephanie&&password
=123,页面空白,代表已经存入缓存
-
浏览器输入 http://localhost:8080/getUser?id=1,可以获得对应的user信息
-
Redis 支持多种数据类型,实体、哈希、列表、集合、有序集合,接下来就是各种类型的操作。
-
RedisService.java
package wen.redisdemo.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.io.Serializable; @Service public class RedisService { @Autowired private RedisTemplate redisTemplate; protected static Logger logger = LoggerFactory.getLogger(RedisService.class); public void set(final String key, Object value) { try { ValueOperations<Serializable, Object> valueOperations = redisTemplate.opsForValue(); valueOperations.set(key, value); } catch (Exception e) { logger.error("set error: key {}, value {}", key, value); } } public Object get(final String key) { ValueOperations<Serializable, Object> valueOperations = redisTemplate.opsForValue(); return valueOperations.get(key); } }
-
RedisDemoApplicationTests.java
package wen.redisdemo; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.*; import org.springframework.test.context.junit4.SpringRunner; import wen.redisdemo.entity.User; import wen.redisdemo.service.RedisService; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @RunWith(SpringRunner.class) @SpringBootTest public class RedisDemoApplicationTests { @Autowired RedisTemplate redisTemplate; @Autowired RedisService redisService; @Test public void contextLoads() { } /** * 取出缓存中的对象 */ @Test public void testObj() { User user = new User(1L, "I", "smile"); ValueOperations<Long, User> valueOperations = redisTemplate.opsForValue(); valueOperations.set(1L, user); User u = valueOperations.get(1L); System.out.println(u.toString()); } /** * 超时失效测试 */ @Test public void testExpire() throws InterruptedException { User user = new User(2L, "I", "smile"); ValueOperations<Long, User> valueOperations = redisTemplate.opsForValue(); valueOperations.set(2L, user, 100, TimeUnit.MILLISECONDS); Thread.sleep(1000); boolean exist_flag = redisTemplate.hasKey(2L); if (exist_flag) { System.out.println("EXIST"); } else { System.out.println("NOT EXIST"); } } /** * 删除数据 */ @Test public void testDelete() { ValueOperations<Long, String> valueOperations = redisTemplate.opsForValue(); User user = new User(3L, "she", "smiles"); redisTemplate.opsForValue().set(3L, user); redisTemplate.delete(3L); boolean exist_flag = redisTemplate.hasKey(3L); if (exist_flag) { System.out.println("EXIST"); } else { System.out.println("NOT EXIST"); } } /** * 哈希表测试 */ @Test public void testHash() { HashOperations<Long, User, String> hash = redisTemplate.opsForHash(); User user = new User(4L, "he", "haha"); hash.put(4L, user, "lala"); String value = (String) hash.get(4L, user); System.out.println(" 4L " + user.toString() + " : " + value); } /** * 队列 */ @Test public void testList() { ListOperations<String, String> list = redisTemplate.opsForList(); list.leftPush("listKey", "first to get in"); list.leftPush("listKey", "second to get in"); list.leftPush("listKey", "third to get in"); String value = (String) list.leftPop("listKey"); System.out.println("list value : " + value.toString()); List<String> values = list.range("listKey", 0, 2); for (String v : values) { System.out.println("list range : " + v); } } /** * set */ @Test public void testSet() { String key = "set"; SetOperations<String, String> set = redisTemplate.opsForSet(); set.add(key, "Hi"); set.add(key, "My"); set.add(key, "Stephanie"); set.add(key, "Stephanie"); Set<String> values = set.members(key); for (String v : values) { System.out.println("set value : " + v); } } /** * 测试difference */ @Test public void TestSetDifference() { SetOperations<String, String> set = redisTemplate.opsForSet(); String key1 = "setOne"; String key2 = "setTwo"; set.add(key1, "a"); set.add(key1, "b"); set.add(key1, "c"); set.add(key1, "c"); set.add(key2, "a"); set.add(key2, "d"); Set<String> differ = set.difference(key1, key2); for (String v : differ) { System.out.println("difference from key1 to key2 : " + v); } } /** * 测试 unions */ @Test public void TestUnions() { SetOperations<String, String> set = redisTemplate.opsForSet(); String key3 = "setThree"; String key4 = "setFour"; set.add(key3, "hi"); set.add(key3, "my"); set.add(key3, "love"); set.add(key4, "Here"); set.add(key4, "I"); set.add(key4, "am"); Set<String> unions = set.union(key3, key4); for (String v : unions) { System.out.println("unions value : " + v); } } /** * 测试 ZSet */ @Test public void testZSet() { String key = "zSetKey"; redisTemplate.delete(key); ZSetOperations<String, String> zSet = redisTemplate.opsForZSet(); zSet.add(key, "hi", 1); zSet.add(key, "how", 3); zSet.add(key, "are", 2); zSet.add(key, "you", 4); Set<String> zSetList = zSet.range(key, 0, 3); for (String v : zSetList) { System.out.println("zSet value : " + v); } Set<String> zSetTwo = zSet.rangeByScore(key, 0, 3); for (String v : zSetTwo) { System.out.println("zSetTwo value : " + v); } } @Test public void testString() throws Exception { redisService.set("Stephanie", "Went"); Assert.assertEquals("Went", redisService.get("Stephanie")); } }
-
Redis 对 Pojo 的支持,新建一个 User 对象,放到缓存中,再取出来
/** * 取出缓存中的对象 */ @Test public void testObj() { User user = new User(1L, "I", "smile"); ValueOperations<Long, User> valueOperations = redisTemplate.opsForValue(); valueOperations.set(1L, user); User u = valueOperations.get(1L); System.out.println(u.toString()); }
-
超时失效:Redis 在存入每一个数据的时候都可以设置超时时间,过了这个时间就会自动删除数据。这种特性非常适合我们对阶段数据的缓存。
-
新建一个 User 对象,存入 Redis 的同时设置 100 毫秒后失效:valueOperations.set(2L, user, 100, TimeUnit.MILLISECONDS);设置一个线程暂停 1000 毫秒之后 Thread.sleep(1000),判断数据是否存在并打印结果。
/** * 超时失效测试 */ @Test public void testExpire() throws InterruptedException { User user = new User(2L, "I", "smile"); ValueOperations<Long, User> valueOperations = redisTemplate.opsForValue(); valueOperations.set(2L, user, 100, TimeUnit.MILLISECONDS); Thread.sleep(1000); boolean exist_flag = redisTemplate.hasKey(2L); if (exist_flag) { System.out.println("EXIST"); } else { System.out.println("NOT EXIST"); } }
-
删除数据,有些情况下需要对过期的缓存进行删除,我们来对 Redis 删除操作进行测试。首先 新建一个 User 对象,紧接着删除此 key 的值,再进行判断
/** * 删除数据 */ @Test public void testDelete() { ValueOperations<Long, String> valueOperations = redisTemplate.opsForValue(); User user = new User(3L, "she", "smiles"); redisTemplate.opsForValue().set(3L, user); redisTemplate.delete(3L); boolean exist_flag = redisTemplate.hasKey(3L); if (exist_flag) { System.out.println("EXIST"); } else { System.out.println("NOT EXIST"); } }
-
hash(哈希):一般我们存储一个键,就会使用 get/set 去存储,实际上这并不是很好的做法。Redis 存储一个 key会有一个最小内存,不管存的这个键多小,都不会低于这个内存,所以合理的使⽤用 Hash 可以帮我们节省很多内存。
-
hash set 就在哈希表 key 中的域(field)的值设为 value。如果 key 不存在,一个新的哈希表被创建并进行hash.set 操作。如果域(field)已经存在于哈希表中,旧值将被覆盖
/** * 哈希表测试 */ @Test public void testHash() { HashOperations<Long, User, String> hash = redisTemplate.opsForHash(); User user = new User(4L, "he", "haha"); hash.put(4L, user, "lala"); String value = (String) hash.get(4L, user); System.out.println(" 4L " + user.toString() + " : " + value); }
-
List:Redis list 的应用场景非常多,也是 Redis 最重要的数据结构之一。
-
List典型的应用场景就是消息队列,可以利用 list 的 PUSH 操作,将任务存在 list 中,然后工作线程再用 POP 操作将任务取出执行。
-
或者通过方法 range 读取队列的一部分。接着上面的例子使用 range 来读取
/** * 队列 */ @Test public void testList() { ListOperations<String, String> list = redisTemplate.opsForList(); list.leftPush("listKey", "first to get in"); list.leftPush("listKey", "second to get in"); list.leftPush("listKey", "third to get in"); String value = (String) list.leftPop("listKey"); System.out.println("list value : " + value.toString()); List<String> values = list.range("listKey", 0, 2); for (String v : values) { System.out.println("list range : " + v); } }
-
Redis set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动排重的
-
set 还提供了判断某个成员是否在一个set 集合内的重要接口,这个也是 list 所不能提供的
-
Redis 为也集合提供了求交集、并集、差集等操作
/** * set */ @Test public void testSet() { String key = "set"; SetOperations<String, String> set = redisTemplate.opsForSet(); set.add(key, "Hi"); set.add(key, "My"); set.add(key, "Stephanie"); set.add(key, "Stephanie"); Set<String> values = set.members(key); for (String v : values) { System.out.println("set value : " + v); } }
-
根据下面这个例子可以看出,difference() 函数会把 key1 中不同于 key2 的数据对比出来、
/** * 测试difference */ @Test public void TestSetDifference() { SetOperations<String, String> set = redisTemplate.opsForSet(); String key1 = "setOne"; String key2 = "setTwo"; set.add(key1, "a"); set.add(key1, "b"); set.add(key1, "c"); set.add(key1, "c"); set.add(key2, "a"); set.add(key2, "d"); Set<String> differ = set.difference(key1, key2); for (String v : differ) { System.out.println("difference from key1 to key2 : " + v); } }
-
根据下面例子可发现,unions 会取两个集合key3、key4的合集
-
set 的内部实现是一个 value 永远为 null 的 HashMap,实际就是通过计算 hash 的方式来快速排重,这也是 set 能提供判断一个成员是否在集合内的原因
/** * 测试 unions */ @Test public void TestUnions() { SetOperations<String, String> set = redisTemplate.opsForSet(); String key3 = "setThree"; String key4 = "setFour"; set.add(key3, "hi"); set.add(key3, "my"); set.add(key3, "love"); set.add(key4, "Here"); set.add(key4, "I"); set.add(key4, "am"); Set<String> unions = set.union(key3, key4); for (String v : unions) { System.out.println("unions value : " + v); } }
-
ZSet : Redis sorted set 的使用场景与 set 类似,区别是 set 不是自动有序的,而 sorted set 可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序,即自动排序。
-
在使用 Zset 的时候需要额外的输入一个参数 score,Zset 会自动根据 Score 的值对集合进行排序,我们可以利用这个特性来做具有权重的队列,比如普通消息的 score 为1,重要消息的 score 为2,然后工作线程可以选择按 score 的倒序来获取工作任务
-
通过下面的例子发现插入到 Zset 的数据会自动根据 score 进行排序,根据这个特性我们可以做优先队列等各种常见的场景。另外 Redis 还提供了 rangeByScore 方法,可以只获取 score 范围内排序后的数据。
-
Redis sorted set 的内部使用 HashMap 和跳跃表(SkipList)来保证数据的存储和有序,HashMap 里放的是成员到 score 的映射,而跳跃表里存放的是所有的成员,排序依据是 HashMap 里存的 score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单
/** * 测试 ZSet */ @Test public void testZSet() { String key = "zSetKey"; redisTemplate.delete(key); ZSetOperations<String, String> zSet = redisTemplate.opsForZSet(); zSet.add(key, "hi", 1); zSet.add(key, "how", 3); zSet.add(key, "are", 2); zSet.add(key, "you", 4); Set<String> zSetList = zSet.range(key, 0, 3); for (String v : zSetList) { System.out.println("zSet value : " + v); } Set<String> zSetTwo = zSet.rangeByScore(key, 0, 3); for (String v : zSetTwo) { System.out.println("zSetTwo value : " + v); } }
-
封装:在实际的使用过程中,一般都会对业务进行简单的包装,最后提供出来对外使用,例如:首先定义一个 RedisService 服务,将 redisTemplate 注入到类中
-
在其他服务使用的时候将 redisService 注入其中,调用对应的方法操作 Redis,这样会具层次感
@Test public void testString() throws Exception { redisService.set("Stephanie", "Went"); Assert.assertEquals("Went", redisService.get("Stephanie")); }
-
总结
-
Redis 是一款非常优秀的高性能缓存中间件
-
Spring Boot 对 Redis 的操作提供了很多的支持
-
Redis 拥有丰富的数据类型,方便在不同的业务场景中使用,特别是提供了很多内置的高效集合操作