本节的目的不在于去教大家理解docker容器(讲docker就脱离了我们课程的核心,我们的课程是Spring Boot 不是docker),而是希望通过docker的方式快速的为大家搭建一个redis数据库,从而方便大家学习使用。
docker search redis docker pull redis:5.0.5 docker images
其实更形象点的理解docker镜像和容器之间的关系,更像是Class类与对象之间的关系。一个类可以构造多个对象,一个镜像可以构造多个容器。类和镜像是实实在在存在的字节码文件;对象和容器是在系统内存里面,作为运行时状态存在。
容器可以运行在内存里面,但是容器存储的数据需要进行持久化。所以在宿主机上创建redis 容器的数据和配置文件存储目录。
# 这里我们在 /home/docker 下创建 mkdir /home/docker/redis/{conf,data} -p cd /home/docker/redis
注意:后面所有的操作命令都要在这个目录/home/docker/redis
下进行
# 获取 redis 的默认配置模版 # 这里主要是想设置下 redis 的 log / password / appendonly # redis 的 docker 运行参数提供了 --appendonly yes 但没 password wget https://gitee.com/hanxt/boot-launch/raw/master/src/main/resources/otherconfig/redis.conf -O conf/redis.conf # 直接替换编辑 sed -i 's/logfile ""/logfile "access.log"/' conf/redis.conf; sed -i 's/# requirepass foobared/requirepass 123456/' conf/redis.conf; sed -i 's/appendonly no/appendonly yes/' conf/redis.conf; sed -i 's/bind 127.0.0.1/bind 0.0.0.0/' conf/redis.conf;
logfile ""
为logfile "access.log"
,指定日志文件名称为access.log
---->指定日志文件的名称# requirepass foobared
为requirepass 123456
,指定访问密码为123456
—>配置登录密码,auth 123456“appendonly no“
为”appendonly yes”
,开启appendonly
模式–》持久化配置IP“bind 127.0.0.1”为“bind 0.0.0.0”
**—>任意ip可以访问protected-mode 是在没有显式定义 bind 地址(即监听全网段),又没有设置密码 requirepass时,protected-mode 只允许本地回环 127.0.0.1 访问。所以改为bind 0.0.0.0
创建并运行一个名为 myredis 的容器,放到start-redis.sh脚本里面
# 创建并运行一个名为 myredis 的容器 docker run \ -p 6379:6379 \ -v $PWD/data:/data \ -v $PWD/conf/redis.conf:/etc/redis/redis.conf \ --privileged=true \ --name myredis \ -d redis:5.0.5 redis-server /etc/redis/redis.conf
# 命令分解 docker run \ -p 6379:6379 \ # 端口映射 宿主机:容器 -v $PWD/data:/data:rw \ # 映射磁盘目录 rw 为读写,宿主机目录:容器目录 -v $PWD/conf/redis.conf:/etc/redis/redis.conf:ro \ # 挂载配置文件 ro 为readonly --privileged=true \ # 给与一些权限 --name myredis \ # 给容器起个名字 -d redis redis-server /etc/redis/redis.conf # deamon 运行容器 并使用配置文件启动容器内的 redis-server
$PWD
是当前目录,也就是/home/docker/redis
# 查看活跃的容器 docker ps # 如果没有 myredis 说明启动失败 查看错误日志 docker logs myredis # 查看 myredis 的 ip 挂载 端口映射等信息 docker inspect myredis # 查看 myredis 的端口映射 docker port myredis
安装好之后,可以进行访问测试
docker exec -it myredis bash redis-cli
上面的测试是在宿主机内访问docker容器。如果在宿主机上可以访问到redis服务,在宿主机之外的主机无法访问该redis服务的话,可能是因为宿主机的防火墙没有打开。参考下面的做法。
开启docker容器所在的宿主机端口,提供给外部服务进行访问
firewall-cmd --zone=public --add-port=6379/tcp --permanent firewall-cmd --reload firewall-cmd --query-port=6379/tcp
Redis 是开源免费, key-value 内存数据库,主要解决高并发、大数据场景下,热点数据访问的性能问题,提供高性能的数据快速访问。
项目中部分数据访问比较频繁,对下游 DB(例如 MySQL)造成服务压力,这时候可以使用缓存来提高效率。
Redis 的主要特点包括:
Redis 作为缓存数据库和 MySQL 这种结构化数据库进行对比。
Map<String,Object>
,key是String类型,value是下面的类型。只不过作为一个独立的数据库单独存在,所以Java中的Map怎么用,redis就怎么用,大同小异。Map<String,String>
Map<String,List<String>>
Map<String,Set<String>>
Map<String,HashMap<String,String>>
上图中命令行更正:lrange,不是lrang
场景一:商品库存数
从业务上,商品库存数据是热点数据,交易行为会直接影响库存。而 Redis 自身 String 类型提供了:
incr key #增加一个库存 decr key # 减少一个库存 incrby key 10 # 增加20个库存 decrby key 15 # 减少15个库存
依此类推的场景:商品的浏览次数,问题或者回复的点赞次数等。这种计数的场景都可以考虑利用 Redis 来实现。
场景二:时效信息存储
Redis 的数据存储具有自动失效能力。也就是存储的 key-value 可以设置过期时间
,SETEX mykey 60 "value"
中的第2个参数就是过期时间。
比如,用户登录某个 App 需要获取登录验证码, 验证码在 30 秒内有效。
list 是按照插入顺序排序的字符串链表。可以在头部和尾部插入新的元素(双向链表实现,两端添加元素的时间复杂度为 O(1)) 。
场景一:消息队列实现
目前有很多专业的消息队列组件 Kafka、RabbitMQ 等。 我们在这里仅仅是使用 list 的特征来实现消息队列的要求。在实际技术选型的过程中,大家可以慎重思考。
list 存储就是一个队列的存储形式:
场景二:最新上架商品
在交易网站首页经常会有新上架产品推荐的模块, 这个模块是存储了最新上架前 100 名。这时候使用 Redis 的 list 数据结构,来进行 TOP 100 新上架产品的存储。
Redis ltrim 指令对一个列表进行修剪(trim),这样 list 就会只包含指定范围的指定元素。
ltrim key start end
start 和 end 都是由 0 开始计数的,这里的 0 是列表里的第一个元素(表头),1 是第二个元素。
如下伪代码演示:
//把新上架商品添加到链表里 ret = r.lpush("new:goods", goodsId) //保持链表 100 位 ret = r.ltrim("new:goods", 0, 99) //获得前 100 个最新上架的商品 id 列表 newest_goods_list = r.lrange("new:goods", 0, 99)
set 也是存储了一个集合列表功能。和 list 不同,set 具备去重功能
(和Java的Set数据类型一样)。当需要存储一个列表信息,同时要求列表内的元素不能有重复,这时候使用 set 比较合适。与此同时,set 还提供的交集、并集、差集。
例如,在交易网站,我们会存储用户感兴趣的商品信息,在进行相似用户分析的时候, 可以通过计算两个不同用户之间感兴趣商品的数量来提供一些依据。
//userid 为用户 ID , goodID 为感兴趣的商品信息。 sadd "user:userId" goodID sadd "user:101" 1 sadd "user:101" 2 sadd "user:102" 1 Sadd "user:102" 3 sinter "user:101" "user:102" # 返回值是1
获取到两个用户相似的产品, 然后确定相似产品的类目就可以进行用户分析。类似的应用场景还有, 社交场景下共同关注好友, 相似兴趣 tag 等场景的支持。
Redis 在存储对象(例如:用户信息)的时候需要对对象进行序列化转换然后存储,还有一种形式,就是将对象数据转换为 JSON 结构数据,然后存储 JSON 的字符串到 Redis。
对于一些对象类型,还有另外一种比较方便的类型,那就是按照 Redis 的 Hash 类型进行存储。
hset key field value
例如,我们存储一些网站用户的基本信息, 我们可以使用:
hset user101 name "小明" hset user101 phone "123456" hset user101 sex "男"
这样就存储了一个用户基本信息,存储信息有:{name : 小明, phone : “123456”,sex : “男”}
当然这种类似场景还非常多, 比如存储订单的数据,产品的数据,商家基本信息等。大家可以参考来进行存储选型。但是不适合存储关联关系比较复杂的数据,那种场景还得用关系型数据库比较方便。
Redis sorted set 的使用场景与 set 类似,区别是 set 不是自动有序的,而 sorted set 可以通过提供一个 score 参数来为存储数据排序
,并且是自动排序,插入既有序。
业务中如果需要一个有序且不重复的集合列表,就可以选择 sorted set 这种数据结构。
比如:商品的购买热度可以将购买总量 num 当做商品列表的 score,这样获取最热门的商品时就是可以自动按售卖总量排好序。
redis集群模式和哨兵模式高可用的安装与运维,需要你去专门的redis课程里面去学习。我们的主要是面向Spring Boot开发人员,不讲redis集群高可用及运维知识。
也就是说,本节为大家介绍的内容是:当架构师或者运维人员将redis 哨兵或cluster集群搭建好之后,在Spring Boot应用中你该如何去连接及使用这些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
为我们隔离了其中的差异性。
引入依赖包
<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全局配置,使用我们前面安装好的测试redis服务。redis的单节点实例,可以通过下面的配置连接redis单节点实例数据库
spring: redis: database: 0 # Redis 数据库索引(默认为 0) host: 192.168.161.3 # Redis 服务器地址 port: 6379 # Redis 服务器连接端口 password: 123456 # Redis 服务器连接密码(默认为空) timeout: 5000 # 连接超时,单位ms lettuce: pool: max-active: 8 # 连接池最大连接数(使用负值表示没有限制) 默认 8 max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 max-idle: 8 # 连接池中的最大空闲连接 默认 8 min-idle: 0 # 连接池中的最小空闲连接 默认 0
redis另外一种非常常用的部署模式是哨兵模式,如果你的公司使用的是这种部署模式,它相对于单实例模式更加的高可用。
需要注意的是,当我们使用spring boot连接哨兵模式的redis集群,连接的是sentinel节点,而不是redis服务实例节点。注意上图的连接顺序。 Application Client是我们的应用程序,sentinel node是哨兵节点。
spring: redis: password: 123456 timeout: 5000 sentinel: # 哨兵模式连接配置 master: mymaster #master节点名称,redis sentinel模式安装的时候会配置 nodes: 192.168.1.201:26379,192.168.1.202:26379,192.168.1.203:26379 # 哨兵的IP:Port列表 lettuce pool: max-active: 8 max-wait: -1 max-idle: 8 min-idle: 0
Redis6—主从复制篇
Redis Cluster是Redis的分布式解决方案,在Redis 3.0版本正式推出的,有效解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构达到负载均衡的目的。分布式集群首要解决问题是:把整个数据集按照分区规则映射到多个节点上,即把数据集按照一定的规则划分到多个节点上,每个节点只保存整个数据集的一个子集。
之前我们为大家介绍的redis安装模式,无论是单节点还是master-slave,其redis服务都保存了数据集的完整副本。cluster模式不是,其redis实例节点只包含完整数据集的子集。
下面的配置,是针对redis集群模式连接访问的配置。
spring: redis: password: 123456 timeout: 5000 database: 0 cluster: #集群模式配置 nodes: 192.168.1.11:6379,192.168.1.12:6379,192.168.1.13:6379,192.168.1.14:6379,192.168.1.15:6379,192.168.1.16:6379 max-redirects: 3 # 重定向的最大次数 lettuce: pool: max-active: 8 max-wait: -1 max-idle: 8 min-idle: 0
Redis–集群
RedisTemplate 的封装使我们能够更方便的进行redis数据操作,比直接使用Jedis或者Lettuce的java SDK要方便很多。RedisTemplate作为java 操作redis数据库的API模板更通用,可以操作所有的redis数据类型。
// 注入RedisTemplate,更通用 @Resource private RedisTemplate<String, Object> redisTemplate; ValueOperations<String,Object> ValueOperations = redisTemplate.opsForValue();//操作字符串 HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();//操作 hash ListOperations<String, Object> listOperations = redisTemplate.opsForList();//操作 list SetOperations<String, Object> setOperations = redisTemplate.opsForSet();//操作 set ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();//操作有序 set
ListOperations、ValueOperations、HashOperations、SetOperations、ZSetOperations等都是针对专有数据类型进行操作,使用起来更简洁。
@Resource(name = "redisTemplate") private ValueOperations<String,Object> valueOperations; //以redis string类型存取Java Object(序列化反序列化) @Resource(name = "redisTemplate") private HashOperations<String, String, Object> hashOperations; //以redis的hash类型存储java Object @Resource(name = "redisTemplate") private ListOperations<String, Object> listOperations; //以redis的list类型存储java Object @Resource(name = "redisTemplate") private SetOperations<String, Object> setOperations; //以redis的set类型存储java Object @Resource(name = "redisTemplate") private ZSetOperations<String, Object> zSetOperations; //以redis的zset类型存储java Object
为了方便后面写代码解释API的使用方法,写测试用例。我们需要先准备数据对象Person,注意要实现Serializable接口,为什么一定要实现这个接口?我们下文解释。
@Data public class Person implements Serializable { private static final long serialVersionUID = -8985545025228238754L; String id; String firstname; String lastname; Address address; //注意这里,不是基础数据类型 public Person(String firstname, String lastname) { this.firstname = firstname; this.lastname = lastname; } }
准备数据对象Address
@Data public class Address implements Serializable { private static final long serialVersionUID = -8985545025228238771L; String city; String country; public Address(String city, String country) { this.city = city; this.country = country; } }
除了RedisTemplate模板类,还有另一个模板类叫做StringRedisTemplate 。二者都提供了用来操作redis数据库的API。
@SpringBootTest public class RedisConfigTest { @Resource private StringRedisTemplate stringRedisTemplate; //以String序列化方式保存数据的通用模板类 @Resource private RedisTemplate<String, Person> redisTemplate; //默认以JDK二进制方式保存数据的通用模板类 @Test public void stringRedisTemplate() { Person person = new Person("kobe","byrant"); person.setAddress(new Address("洛杉矶","美国")); //将数据存入redis数据库 stringRedisTemplate.opsForValue().set("player:srt","kobe byrant",20, TimeUnit.SECONDS); redisTemplate.opsForValue().set("player:rt",person,20, TimeUnit.SECONDS); } }
二者的区别在于
List
类型为例:RedisTemplate
操作List< Object >
,StringRedisTemplate
操作List< String >
RedisTemplate
使用的是JdkSerializationRedisSerializer
存入数据会将数据先序列化成字节数组然后在存入Redis
数据库。StringRedisTemplate
使用的是StringRedisSerializer
回答上文中的问题,redis持久化的java数据类为什么要实现Serializable接口?因为RedisTemplate默认使用的是JdkSerializationRedisSerializer,也就是使用Java JDK默认的序列化方式存储数据。如果不实现Serializable接口,JDK序列化就会报错,这是java基础知识。如果我们可以不使用JDK默认的序列化方式,就不需要实现这个Serializable接口。
需要注意的是因为
RedisTemplate
和StringRedisTemplate
的默认序列化存储方式
不一样,所以二者存储的数据并不能通用。也就是说RedisTemplate存的数据只能用RedisTemplate去取,对于StringRedisTemplate也是一样。
其实这个不是严格意义上的乱码,是JDK的二进制序列化之后的存储方式。人看不懂,但是程序是能看懂的。
那有没有人一种人能看懂,程序也能看懂的序列化结果?看下文的配置类代码
StringRedisSerializer
对key
进行序列化(字符串格式)Jackson2JsonRedisSerializer
对value
将进行序列化(JSON格式)@Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); //重点在这四行代码 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
乱码问题的症结在于对象的序列化问题:RedisTemplate
默认使用的是JdkSerializationRedisSerializer
(二进制存储),StringRedisTemplate
默认使用的是StringRedisSerializer
(redis字符串格式存储)。
序列化方式对比:
JdkSerializationRedisSerializer
: 使用JDK
提供的序列化功能。 优点是反序列化时不需要提供类型信息(class
),但缺点是需要实现Serializable
接口,还有序列化后的结果非常庞大,是JSON
格式的5倍左右,这样就会消耗redis
服务器的大量内存。而且是以二进制形式保存,自然人无法理解。Jackson2JsonRedisSerializer
: 使用Jackson
库将对象序列化为JSON
字符串。优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable
接口。似乎没啥缺点。StringRedisSerializer
序列化之后的结果,自然人也是可以理解,但是value
只能是String
类型,不能是Object
。下面的各种数据类型操作的api和redis命令行api的含义几乎是一致的。
@SpringBootTest public class RedisConfigTest2 { @Resource(name = "redisTemplate") private ValueOperations<String,Object> valueOperations; //以redis string类型存取Java Object(序列化反序列化) @Resource(name = "redisTemplate") private HashOperations<String, String, Object> hashOperations; //以redis的hash类型存储java Object @Resource(name = "redisTemplate") private ListOperations<String, Object> listOperations; //以redis的list类型存储java Object @Resource(name = "redisTemplate") private SetOperations<String, Object> setOperations; //以redis的set类型存储java Object @Resource(name = "redisTemplate") private ZSetOperations<String, Object> zSetOperations; //以redis的zset类型存储java Object @Test public void testValueObj() { Person person = new Person("boke","byrant"); person.setAddress(new Address("南京","中国")); //向redis数据库保存数据(key,value),数据有效期20秒 valueOperations.set("player:1",person,20, TimeUnit.SECONDS); //20秒之后数据消失 //根据key把数据取出来 Person getBack = (Person)valueOperations.get("player:1"); System.out.println(getBack); } @Test public void testSetOperation() { Person person = new Person("kobe","byrant"); Person person2 = new Person("curry","stephen"); setOperations.add("playerset",person,person2); //向Set中添加数据项 //members获取Redis Set中的所有记录 Set<Object> result = setOperations.members("playerset"); System.out.println(result); //包含kobe和curry的数组 } @Test public void HashOperations() { Person person = new Person("kobe","byrant"); //使用hash的方法存储对象数据(一个属性一个属性的存,下节教大家简单的方法) hashOperations.put("hash:player","firstname",person.getFirstname()); hashOperations.put("hash:player","lastname",person.getLastname()); hashOperations.put("hash:player","address",person.getAddress()); //取出一个对象的属性值,有没有办法一次将整个对象取出来?有,下节介绍 String firstName = (String)hashOperations.get("hash:player","firstname"); System.out.println(firstName); //kobe } @Test public void ListOperations() { //将数据对象放入队列 listOperations.leftPush("list:player",new Person("kobe","byrant")); listOperations.leftPush("list:player",new Person("Jordan","Mikel")); listOperations.leftPush("list:player",new Person("curry","stephen")); //从左侧存,再从左侧取,所以取出来的数据是后放入的curry Person person = (Person) listOperations.leftPop("list:player"); System.out.println(person); //curry对象 } }
RedisTemplate操作Redis,这一篇文章就够了(一)
redis原生数据类型操作大全
通过集成spring-boot-starter-data-redis
之后一共有三种redis hash
数据操作方式可以供我们选择
Jackson2HashMapper
存取对象RedisRepository
的对象操作@Test public void HashOperations() { Person person = new Person("kobe","byrant"); person.setAddress(new Address("洛杉矶","美国")); //使用hash的方法存储对象数据(一个属性一个属性的存,下节教大家简单的方法) hashOperations.put("hash:player","firstname",person.getFirstname()); hashOperations.put("hash:player","lastname",person.getLastname()); hashOperations.put("hash:player","address",person.getAddress()); String firstName = (String)hashOperations.get("hash:player","firstname"); System.out.println(firstName); }
数据在redis数据库里面存储结构是下面这样的
上一小节我们操作hash对象的时候是一个属性一个属性设置的,那我们有没有办法将对象一次性hash入库呢?
我们可以使用jacksonHashOperations
和Jackson2HashMapper
import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest public class RedisConfigTest3 { @Resource(name="redisTemplate") private HashOperations<String, String, Object> jacksonHashOperations; //注意这里的false,下文会讲解 private HashMapper<Object, String, Object> jackson2HashMapper = new Jackson2HashMapper(false); @Test public void testHashPutAll(){ Person person = new Person("kobe","bryant"); person.setId("kobe"); person.setAddress(new Address("洛杉矶","美国")); //将对象以hash的形式放入redis数据库 Map<String,Object> mappedHash = jackson2HashMapper.toHash(person); jacksonHashOperations.putAll("player:" + person.getId(), mappedHash); //将对象从数据库取出来 Map<String,Object> loadedHash = jacksonHashOperations.entries("player:" + person.getId()); Object map = jackson2HashMapper.fromHash(loadedHash); Person getback = new ObjectMapper().convertValue(map,Person.class); //Junit5,验证放进去的和取出来的数据一致 assertEquals(person.getFirstname(),getback.getFirstname()); } }
使用这种方式可以一次性的存取java 对象为redis数据库的hash数据类型。需要注意的是:执行上面的测试用例,Person和Address一定要有public无参构造方法,在将map转换成Person或Address对象的时候用到,如果没有的话会报错。
new Jackson2HashMapper(false)
,注意属性对象Address
的存储格式(两张图对比观察)new Jackson2HashMapper(true),
注意属性对象Address的存储格式(两张图对比观察)
需要注意的是:使用这种方法存储hash数据,需要多出一个键值对@class
说明该hash
数据对应的java
类。在反序列化的时候会使用到,用于将hash
数据转换成java
对象。
下面为大家介绍使用RedisRepository进行redis数据操作,它不只是能简单的存取数据,还可以做很多的CURD操作。使用起来和我们用JPA进行关系型数据库的单表操作,几乎是一样的。
首先,我们需要在需要操作的java
实体类上面加上@RedisHash
注解,并使用@Id
为该实体类指定id
。是不是和JPA
挺像的?
@RedisHash("people") //注意这里的person,下文会说明 public class Person { @Id String id; //其他和上一节代码一样 }
然后写一个PersonRepository ,继承CrudRepository,是不是也和JPA差不多?
//泛型第二个参数是id的数据类型 public interface PersonRepository extends CrudRepository<Person, String> { // 继承CrudRepository,获取基本的CRUD操作 }
CrudRepository默认为我们提供了下面的这么多方法,我们直接调用就可以了。
在项目入口方法上加上注解@EnableRedisRepositories
(笔者测试,在比较新的版本中这个注解已经不需要添加了,默认支持),然后进行
下面的测试
@SpringBootTest public class RedisRepositoryTest { @Resource PersonRepository personRepository; @Test public void test(){ Person rand = new Person("zimug", "汉神"); rand.setAddress(new Address("杭州", "中国")); personRepository.save(rand); //存 Optional<Person> op = personRepository.findById(rand.getId()); //取 Person p2 = op.get(); personRepository.count(); //统计Person的数量 personRepository.delete(rand); //删除person对象rand } }
测试结果:需要注意的是RedisRepository在存取对象数据的时候,实际上使用了redis的2种数据类型
@RedisHash("people")指定的。
绝大多数情况下,关系型数据库select查询是出现性能问题最大的地方。一方面,select 会有很多像 join、group、order、like 等这样丰富的语义,而这些语义是非常耗性能的;另一方面,大多数应用都是读多写少,所以加剧了慢查询的问题。
分布式系统中远程调用也会耗很多性能,因为有网络开销,会导致整体的响应时间下降。为了挽救这样的性能开销,在业务允许的情况(不需要太实时的数据)下,使用缓存是非常必要的事情。
当用户请求增多时,数据库的压力将大大增加,通过缓存能够大大降低数据库的压力。
使用缓存最关键的一点就是保证:缓存与数据库的数据一致性,该怎么去做?下图是一种最常用的缓存操作模式,来保证数据一致性。
更新写数据
:先把数据存到数据库中,然后再让缓存失效或更新。缓存操作失败,数据库事务回滚。删除写数据
: 先从数据库里面删掉数据,再从缓存里面删掉。缓存操作失败,数据库事务回滚。查询读数据
缓存命中
:先去缓存 cache 中取数据,取到后返回结果。缓存失效
:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,在将数据放到缓存中。如果上面的这些更新、删除、查询操作流程全都由程序员通过编码来完成的话
Spring cache相关注解
我们可以使用Spring cache解决上面遇到的两个问题,Spring cache通过注解的方式来操作缓存,一定程度上减少了程序员缓存操作代码编写量。注解添加和移除都很方便,不与业务代码耦合,容易维护。
第一步:pom.xml 添加 Spring Boot 的 jar 依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
第二步:添加入口启动类 @EnableCaching
注解开启 Caching,实例如下。
@EnableCaching
在Spring Boot
中通过@EnableCaching
注解自动化配置合适的缓存管理器(CacheManager
),Spring Boot
根据下面的顺序去侦测缓存提供者,也就是说Spring Cache支持下面的这些缓存框架:
下面的例子第一次访问走数据库(代码上断点断下来),第二次访问就走缓存了(不走函数代码)。可以自己下断点试一下。
@Cacheable(value="article") @GetMapping( "/article/{id}") public @ResponseBody AjaxResponse getArticle(@PathVariable Long id) {
使用redis缓存,被缓存的对象(函数返回值)有几个非常需要注意的点:
让缓存使用JDK默认的序列化和反序列化方式非常不友好,我们完全可以修改为使用JSON序列化与反序列化的方式,可读性更强,体积更小,速度更快
@Configuration public class RedisConfig { //这个函数是上一节的内容 @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); //重点在这四行代码 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } //本节的重点配置,让Redis缓存的序列化方式使用redisTemplate.getValueSerializer() //不在使用JDK默认的序列化方式 @Bean public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) { RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory()); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer())); return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration); } }
一定要把这张图理解的非常透彻,才能把缓存注解用好。
仍然以我们之前一直使用的ArtivleServiceImpl为例(包含增删改查方法),添加缓存注解。
被@Cacheable
注解的方法,在第一次被请求的时候执行方法体,并将方法的返回值放入缓存。在第二次请求的时候,由于缓存中已经包含该数据,将不执行被注解的方法的方法体,直接从缓存中获取数据。对于查询过程的缓存操作,要满足上图中的蓝色箭头线指引的操作流程,所有的操作流程只需要加上一个@Cacheable
就可以实现。
public static final String CACHE_OBJECT = "article"; //缓存名称 @Cacheable(value = CACHE_OBJECT,key = "#id") //这里的value和key参考下面的redis数据库截图理解 public ArticleVO getArticle(Long id) { return dozerMapper.map(articleMapper.selectById(id),ArticleVO.class); }
需要注意的是:缓存注解的key是一个SPEL表达式
,“#id”表示获取函数的参数id的值作为缓存的key值
。如果参数id=1,那么最终redis缓存的key就是:“article::1”。下图是redis缓存数据库中这条缓存记录的截图:
大家要注意Object
和List<Object>
是两种不同的业务数据,所以对应的缓存也是两种缓存。注意下文中,缓存注解的key是字符串list,因为缓存注解默认使用SPEL表达式,如果我们想使用字符串需要加上斜杠。
public static final String CACHE_LIST_KEY = "\"list\""; @Cacheable(value = CACHE_OBJECT,key = CACHE_LIST_KEY) public List<ArticleVO> getAll() { List<Article> articles = articleMapper.selectList(null); return DozerUtils.mapList(articles,ArticleVO.class); }
对于查询过程的缓存操作,要满足上图中的蓝色箭头线指引的操作流程,所有的操作流程只需要加上一个@Cacheable
就可以实现。目前MySQL数据库的article表有4条数据,所以缓存结果是一个包含4个article元素的数组
如下面的代码所示,将在函数执行成功之后删除redis key为“article::1”的缓存(假设删除id=1的记录)。
@Override @Caching(evict = { @CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY), //删除List集合缓存 @CacheEvict(value = CACHE_OBJECT,key = "#id") //删除单条记录缓存 }) public void deleteArticle(Long id) { articleMapper.deleteById(id); }
@CacheEvict
,所以我们用@Caching
注解把两个@CacheEvict
包起来。@CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY) //删除List集合缓存 public void saveArticle(ArticleVO article) { Article articlePO = dozerMapper.map(article, Article.class); articleMapper.insert(articlePO); }
执行完成上面的方法,MySQL数据库新增了一条article记录;成功之后在《1.2.集合对象的查询缓存》缓存到redis中的key为“article::list”的缓存也将被删除。
注意更新对象的时候,我们在该方法上面加了两个缓存注解。
CachePut
注解的作用是在方法执行成功之后,将其返回值放入缓存。key ="#article.getId()"
表示使用参数article
的id
属性作为缓存key
。CacheEvict
注解用于将“article::list”
的缓存删除,因为某一条记录的数据更新,就表示原来缓存的List
集合数据与MySQL
数据库中的数据不一致,所以把它删除掉。缓存数据可以没有,但是不能和后端被缓存的关系数据库数据不一致。@CachePut(value = CACHE_OBJECT,key = "#article.getId()") @CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY) public ArticleVO updateArticle(ArticleVO article) { Article articlePO = dozerMapper.map(article,Article.class); articleMapper.updateById(articlePO); return article; //为了保证一致性,最后返回的更新结果,最好从数据库去查 }
执行完成该方法,假如ArticleVO参数对象的id=1
需要特别注意的是:如果在更新方法上使用CachePut
注解,该方法一定要有数据更新之后返回值,因为返回值就是缓存值
。
比较简单的做法是直接将不一致的缓存删掉,而不是去更新缓存。
这样操作对于程序员的要求更低,不容易出错。
缓存数据可以没有,但是不能和后端被缓存的关系数据库数据不一致。
@Override @Caching(evict = { @CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY), //删除List集合缓存 @CacheEvict(value = CACHE_OBJECT,key = "#article.getId()") //删除单条记录缓存 }) public void updateArticle(ArticleVO article) { Article articlePO = dozerMapper.map(article,Article.class); articleMapper.updateById(articlePO); }
方法不需要有返回值。执行完成该方法,假如ArticleVO参数对象的id=1
@Cacheable
通常应用到读取数据的查询方法上:先从缓存中读取,如果没有再调用方法获取数据,然后把数据查询结果添加到缓存中。如果缓存中查找到数据,被注解的方法将不会执行。
@CachePut
通常应用于修改方法配置,能够根据方法的请求参数对其注解的函数返回值进行缓存,和 @Cacheable
不同的是,它每次都会触发被注解方法的调用。
@CachEvict
通常应用于删除方法配置,能够根据一定的条件对缓存进行删除。可以清除一条或多条缓存。
在实际的生产环境中,没有一定之规,哪种注解必须用在哪种方法上,@CachEvict
注解通常也用于更新方法上。数据的缓存策略,要根据资源的使用方式,做出合理的缓存策略规划。保证缓存与业务数据库的数据一致性。并做好测试,对于缓存的正确使用,测试才是王道!