1、安装
linux服务器直接输入以下命令 ( 下载,安装,编译)
注意:我是直接opt目录下解压的
wget http://download.redis.io/releases/redis-6.0.6.tar.gz ## wget获取指定位置的包
tar xzf redis-6.0.6.tar.gz ## 解压.gz包,x解压抽取的意思,f指定文件
cd redis-6.0.6
进入到redis目录下 编译文件
make
注意:如果编译过程中出现问题,那么是不能正确的启动redis的
一般情况下编译出现问题 是本机的gcc版本太低,需要升级一下,输入以下命令
# sudo 借用root权限执行 (权限太低执行不了)
sudo yum install centos-release-scl
sudo yum install devtoolset-7-gcc*
scl enable devtoolset-7 bash
更新完后,就可以
重新进入 redis根目录下make
了
接着 make install
通过 redis/src/redis-server 启动服务器
通过 redis/src/redis-cli 启动客户端
2、配置 redis.config
1. 我们可以直接在 opt下 直接启动redis ,也可以在/usr/bin 下找到对应的redis命令 启动redis
2. 将安装目录下的redis的配置文件 redis.config配置到自己的配置文件目录下,我们通过指定的配置redis.config 来启动redis
3. 修改reids配置文件,使其默认通过后台进程启动
4. 启动 通过指定的配置文件
3、配置redis数据库
通过查看redis.config 配置文件可以看到 默认数据库是16个,按索引排列
4、基本使用
-
通过set 设key value 。通过get key 取对应的key 取value ,使用dbsize 查看当前数据库 数据个数。
-
通过 del key … 可以删除多个key对应的value ,integer表示 删除成功数
-
exists key 判断对应的key是否存在,nil表示null
- expire key second 设置对应key的过期时间,ttl key 查看key过期没有,如果过期时间到了redis会移除过期key
- ** keys * ** 查看当前数据库中,所有的key ,keys xxx* 查看当前数据库中以xxx开头的key
-
不同数据库数据是独立的,目前不能从其它数据库查数据
select 数据库编号 ## 选择数据库
- flushall 清除所有数据库数据,flushdb清除当前数据库数据
-
move key index 将指定的key 转移到指定的数据库
move name 1
-
persist key 移除key的生存时间 使key永久生效
redis> SET mykey "Hello"
OK
redis> EXPIRE mykey 10 # 为 key 设置生存时间
(integer) 1
redis> TTL mykey
(integer) 10
redis> PERSIST mykey # 移除 key 的生存时间
(integer) 1
redis> TTL mykey
(integer) -1
- mset 设置多个key value , mget 获取多个value,randomkey 随机取key
-
type key 返回数据的 数据类型
返回 key 的数据类型,数据类型有:
- none (key不存在)
- string (字符串)
- list (列表)
- set (集合)
- zset (有序集)
- hash (哈希表)
-
select index 切换数据库
select 1
5、字符串(String)
-
getRange key start end 获取指定key指定范围的值 注意与java不同这是 左右都是闭包【】
-
getset key value 先获取key的值,在设置value
-
setex (expire)key second value 设置key的存在时限。**
setnx (not exists)key value** 设置一个数据库中没有的key, 如果该key存在设置失败
(如果存在的,会覆盖原有的key,并给与新的生存时间)
-
setRange key index newValue 为指定key的值修改数据 从index位置开始
-
incr key 实现key值的自增 注意:key的值value 必须是数字,但还是以字符串保存
-
incrBy key increment 对key值指定增量自增 , decrBy,decr 同理
-
append 实现数据追加
-
set key value
set user:1 {username:zhangsan,age:1}
6、哈希(hash) 命令(几乎以H开头)
redis hash 是一个(String类型的)field 和 value 的一个映射表,一般用来存储对象。(一个hash表 表示一个对象,一个对象中有多个 k,v值(f,v值))
Redis 中每个 hash 可以存储 2的32次方 - 1 个键值对(40多亿)
可以将hash理解成 一张表 表有多个键值对组成
-
创建一张 hash表作为key 里面封装这多个key value
-
hdel key(表) field ,hget key field , hset key field value 从myhash表中删除field对应的值
-
hexists key(表) field ,从hash表中 查看对应field字段是否存在,存在打印1,否则0
-
hgetAll key(表) 从hash表中 获取所有的字段 和值
-
hkeys key 获取表中所有的 field
-
hLen key (表) 获取表中数据的数量
-
HincrBy key(表) field increment 对指定hash表中某一字段 增量相加 没有key的话会自动创建哦
HincrByFloat key field increment 对某一字段值 进行浮点增量增加
-
Hmget 获取hash表中 多个指定字段的值
Hmset 往hash表 中 设置多个值
- Hvals key 返回hash表中 所有字段的值
7、列表(list) 命令(几乎以L开头)
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
一个列表最多可以包含 2的32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
注意:虽然list 本质上是一个key 但是不能用 get lists 获取list中的值
- Blpop key timeout (list表) 从列表左边中弹出一个值,并指定超时时间,若列表中没有值,会阻塞列表,直到超时后才打印nil(null)/直到有元素 进入列表
-
BRpop 同理
-
Lpush key(表) Rpush key 在表的左右两端,往表放值
- Lpop Rpop 同理 取值
- Llen key 获取list长度
- Lrange key start end 获取指定list范围中 的 数据
- Ltrim key start end 对指定范围的数据 修剪保留
- 剩下的具体看文档
8、集合(set)(命令以S开头)
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
-
sadd key (set集)value… 往set中存放多个值 , smembers key 输出set中全部的元素
-
scard key 输出set中元素的数量
-
sdiff key1 key2 比较两个集合的差异
redis> SADD key1 "a" (integer) 1 redis> SADD key1 "b" (integer) 1 redis> SADD key1 "c" (integer) 1 redis> SADD key2 "c" (integer) 1 redis> SADD key2 "d" (integer) 1 redis> SADD key2 "e" (integer) 1 redis> SDIFF key1 key2 1) "a" 2) "b"
-
sinter key 1 key2 取两个集合的交集
-
sInterStore destination key1 key2 取两个集合的交集 存储在 destination中
redis 127.0.0.1:6379> SADD myset1 "hello" (integer) 1 redis 127.0.0.1:6379> SADD myset1 "foo" (integer) 1 redis 127.0.0.1:6379> SADD myset1 "bar" (integer) 1 redis 127.0.0.1:6379> SADD myset2 "hello" (integer) 1 redis 127.0.0.1:6379> SADD myset2 "world" (integer) 1 redis 127.0.0.1:6379> SINTERSTORE myset myset1 myset2 (integer) 1 redis 127.0.0.1:6379> SMEMBERS myset 1) "hello"
-
Spop key count 随机中set中取出count个 数据
-
sunion key1 key2 合并两个集合
-
SRandMember key 【count】(可选) 随机从set中 返回几个数据 (不删除数据)
-
补充看官网
9、有序集合(sorted set) (命令以Z开头)
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
-
zadd key score member… 向zset集合中 插入一些值,附带double/int数(用来比较大小排序)
-
zrange key start end withScores 列出zset集合的指定位置元素 并携带分数
-
zcard key 算出zset中的集合个数
- zrem key member 移除zset集合中的指定元素
redis 127.0.0.1:6379> ZRANGE page_rank 0 -1 WITHSCORES
1) "bing.com"
2) "8"
3) "baidu.com"
4) "9"
5) "google.com"
6) "10"
# 移除单个元素
redis 127.0.0.1:6379> ZREM page_rank google.com
(integer) 1
redis 127.0.0.1:6379> ZRANGE page_rank 0 -1 WITHSCORES
1) "bing.com"
2) "8"
3) "baidu.com"
4) "9"
- 其它需求看文档
10、HyperLogLog 基数统计算法
什么是基数?(不重复的数据元素的个数)
1、比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
为什么要用hyperLogLog 算法?
-
HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。(占用空间小)
-
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
pfadd key element 往key中放元素 ,注意:不能放重复的(与set/zset类似)
pfcount key 计算key中的基数(不重复的数据个数)
pfMerge hyper hyper1 hyper2 合并多个hyperloglog 成一个hyper
11、Geospatial 地理位置
Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。
Redis GEO 操作方法有:
-
geoadd:添加地理位置的坐标。 (要使用必先 导入对应的全部地理坐哦)
-
geopos:获取地理位置的坐标。
-
geodist:计算两个位置之间的距离。
-
georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
-
georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
-
geohash:返回一个或多个位置对象的 geohash 值。
-
geoAdd 可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
往china:city 表 中添加一些城市 和经纬度
-
geoPos 返回 表中 指定名称的 经纬度
- geodist(distance) 返回表中两个地理位置的直线距离 可以指定单位
- geoRadius key 经度 纬度 半径 单位 注意:是可以写多个条件的
-
geoRadiusByMember 更geoRadius同样的用法 只不过不是经纬度定位中心 而是以表中成员来定位中心(不过好像不稳)
-
geospatial底层采用的是zset 因此可以用对应的zset命令来删除对应的数据
- 删除 key
12、BitMaps (位图)
-
位图一般由二进制0,1表示,常用来显示两种状态。例如:打卡,未打卡。迟到,未迟到
具体信息用时再查
13、Redis 事务
redis 事务可以一次执行多个命令,并带有三个保证
- exec命令执行前,多个命令被放入队列缓存
- exec命令执行后,缓存队列中的命令顺序执行,一旦有一个有误,不影响其它命令的执行
- 在事务执行过程中,其它客户端提交的命令请求不会插入到当前的缓存命令队列
redis事务执行的三个阶段
- 开启事务(multi)
- 命令入队(queue)
- 执行事务(exec)
redis事务和mysql事务是有区别的
-
redis 事务并不具有原子性,一旦事务中(命令队列中)有一命令执行失败,并不影响整个事务的执行
但是redis单条命令具有原子性
-
mysql事务具有原子性,在一个事务中,多条命令,一旦有一条执行失败,其它全部失败
实例:
redis事务
1、编译时异常 ,在redis事务中,编译时语法错误,会导致整个事务都不能执行
3、运行时异常 ,在redis事务中,运行时异常,运行时报错,但整体事务依旧能正常执行
事务的ACID原则
事务具有4个特征,分别是原子性、一致性、隔离性和持久性,简称事务的ACID特性;
一、原子性(atomicity)
一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中的一部分操作,这就是事务的原子性
二、一致性(consistency)
事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。
如果数据库系统在运行过程中发生故障,有些事务尚未完成就被迫中断,这些未完成的事务对数据库所作的修改有一部分已写入物理数据库,这是数据库就处于一种不正确的状态,也就是不一致的状态
三、隔离性(isolation)
事务的隔离性是指在并发环境中,并发的事务时相互隔离的,一个事务的执行不能不被其他事务干扰。不同的事务并发操作相同的数据时,每个事务都有各自完成的数据空间,即一个事务内部的操作及使用的数据对其他并发事务时隔离的,并发执行的各个事务之间不能相互干扰。
在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同,分别是:未授权读取,授权读取,可重复读取和串行化
1、读未提交(Read Uncommited),该隔离级别允许脏读取,其隔离级别最低;比如事务A和事务B同时进行,事务A在整个执行阶段,会将某数据的值从1开始一直加到10,然后进行事务提交,此时,事务B能够看到这个数据项在事务A操作过程中的所有中间值(如1变成2,2变成3等),而对这一系列的中间值的读取就是未授权读取
2、授权读取也称为已提交读(Read Commited),授权读取只允许获取已经提交的数据。比如事务A和事务B同时进行,事务A进行+1操作,此时,事务B无法看到这个数据项在事务A操作过程中的所有中间值,只能看到最终的10。另外,如果说有一个事务C,和事务A进行非常类似的操作,只是事务C是将数据项从10加到20,此时事务B也同样可以读取到20,即授权读取允许不可重复读取。
3、可重复读(Repeatable Read)
就是保证在事务处理过程中,多次读取同一个数据时,其值都和事务开始时刻是一致的,因此该事务级别禁止不可重复读取和脏读取,但是有可能出现幻影数据。所谓幻影数据,就是指同样的事务操作,在前后两个时间段内执行对同一个数据项的读取,可能出现不一致的结果。在上面的例子中,可重复读取隔离级别能够保证事务B在第一次事务操作过程中,始终对数据项读取到1,但是在下一次事务操作中,即使事务B(注意,事务名字虽然相同,但是指的是另一个事务操作)采用同样的查询方式,就可能读取到10或20;
4、串行化
是最严格的事务隔离级别,它要求所有事务被串行执行,即事务只能一个接一个的进行处理,不能并发执行。
四、持久性(durability)
一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中。–即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束的状态
14、锁 (redis 实现悲观锁和乐观锁)
1、 什么是悲观锁和乐观锁?
- 悲观锁:见名知意,很悲观,担心数据会被修改,对读写操作都上锁,自己用完数据后,就会进行解锁。那么其它线程进行操作时必须等待,效率相对较低。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等。都是在操作之前先上锁让别人无法操作该数据。
- 乐观锁:顾名思义,很乐观,每次取数据的时候并不担心自己的数据会被修改。数据库实现乐观锁,通常使用的是version(数据版本),来表示数据的。每次取数据的时候,连同数据版本version一起取出。当读取出数据,对数据进行更改时,会将数版本version 加一,然后提交到数据库,此时比较数据版本version,如果提交的数据版本version大于当前数据库对应记录的数据版本version,那么提交成功。否则小于等于都会提交失败。需要重新从数据库取数据。
2、 乐观锁 和悲观锁的 应用场景
- 乐观锁适用于 频繁读取数据的场景,因为读取数据并不会上锁。但是当有大量数据写入的时候,会频繁的提交不成功,会重新读取数据,再提交。
- 悲观锁适用于 频繁写入数据的场景,因为不管是读还是写 都会上锁,如果大量写入数据,为了数据安全上锁是有必要的,相反乐观锁就会大量的读取提交操作。但是当有大量数据读出的时候,效率低下。
3、 redis 实现乐观锁
- redis通过watch实现乐观锁,当对一个key进行监控的时候,客户端A一个事务正在修改key,客户端B已经修改完了key。那么客户端A的当前事务,全部执行失败。因为在事务过程中,key的值已经被修改,当前事务discard(失败),如果新开一个事务是在对key进行操作是没有问题的,并且不开启事务直接执行命令也是没有问题的,因为redis单条命令具有原子性
客户端A
客户端B
一旦监控的key的数据被另一客户端修改,当前客户端 对key的事务操作全部执行失败。
15、springboot集成redis
1、 导包
<!--springboot中的redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lettuce pool 缓存连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2、 在RedisAutoConfiguration中找到对应的RedisProperties配置,在yml中配置基本配置
spring:
redis:
host: xxxxxx 自己的ip
port: 6379
lettuce:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
3、从RedisAutoConfiguration中看到默认配置了两个bean RedisTemplate,StringRedisTemplate 而且是基于lettuce是redis-cli客户端
获取redisTemplate进行测试即可
从这里看是测试成功的。但是我们从服务器上看对应的key,却是这个样子
怎么办?
原因: 大概是lettuce客户端连接redis服务器采用的是byte数组,因此会将对象先转化为byte数组,而转换方法是使用JdkSerializationRedisSerializer 的convert方法转换,因此需要替换到默认的redisSerializaer
4、 配置redisConfig
解决
自己配置redisConfig,不使用默认的
@Configuration
@EnableCaching
public class RedisConfig {
/**
* 配置自定义redisTemplate
* @return
*/
@Bean
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// 设置值(value)的序列化采用Jackson2JsonRedisSerializer。
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// 设置键(key)的序列化采用StringRedisSerializer。
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
idea 客户端再次运行结果:
服务器 客户端运行结果:( 注意:中文内容还是会乱码)
16、Redis发布订阅
1、 发布订阅是什么?
-
发布订阅是一种通信模式。分为发布者(publish) 频道(channel) 订阅者(subscribe)
-
发布者需要往指定的频道发送数据消息,如果这一频道存在多个订阅者,那么频道会把消息发送给订阅者。
2、测试实例
两个订阅者,一个发布者,一个频道
17、Redis持久化 RDB(Redis DataBase)和AOF(Append Only File)
问题:为什么要持久化呢?
redis是缓存/内存数据库,断电数据即失。我们有必要对数据进行定期的持久化,防止数据丢失
RDB机制
RDB本质
RDB其实就是把数据以快照的形式保存在磁盘上。什么是快照呢,你可以理解成把当前时刻的数据拍成一张照片保存下来。
既然RDB机制是通过把某个时刻的所有数据生成一个快照来保存,那么就应该有一种触发机制,是实现这个过程。对于RDB来说,提供了三种机制:save、bgsave、自动化。会将数据持久化的dump.rdb中,重启服务器的时候,会活化其中的数据,redis启动时会把./ 目录(命令所在目录)下的dump.rdb 中的数据恢复。
RDB三种触发方式
-
save触发(同步,会堵塞)
该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。具体流程如下:
执行完成时候如果存在老的RDB文件,就把新的替代掉旧的。我们的客户端可能都是几万或者是几十万,save命令这种方式显然不可取。
-
bgsave触发方式(异步)
执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体流程如下:
具体操作是
1、当client进行bgsave触发RDB机制时2、Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。
2、阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。
-
配置文件配置,自动触发
自动触发是由我们的配置文件来完成的。在redis.conf配置文件中,里面有如下配置,我们可以去设置:
①save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
默认如下配置:
1、表示900 秒内如果至少有 1 个 key 的值变化,则保存
2、表示300 秒内如果至少有 10 个 key 的值变化,则保存
3、表示60 秒内如果至少有 10000 个 key 的值变化,则保存不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能。注意:redis内部默认使用bgsave命令来触发持久化
②stop-writes-on-bgsave-error :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了
③rdbcompression ;默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。
④rdbchecksum :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
⑤dbfilename :设置快照的文件名,默认是 dump.rdb
⑥dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。
3、 RDB的优势与劣势
-
优势:
- rdb只备份数据,占用空间相对较小,回恢复数据也快
- rdb默认采用的bgsave来实现,fork一个子进程进行数据持久化
- rdb支持全量备份
-
劣势
- RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。(简而言之,fork期间数据会丢失)
- 全量备份耗时严重
AOF机制(每收到一条指令就会对文件进行追加)
AOF(Append Only File)原理
全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。(效率低,但是安全)
1、持久化原理
他的原理看下面这张图:
每当有一个写命令过来时,就直接保存在我们的AOF文件中。
2、文件重写原理
AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。
重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
AOF也有三种触发机制
(1)每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好
(2)每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失
(3)不同no:从不同步
优点
(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
(2)AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。
(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据
缺点
(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的
(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。
持久化机制的选择
RDB和AOF到底该如何选择
选择的话,两者加一起才更好。因为两个持久化机制你明白了,剩下的就是看自己的需求了,需求不同选择的也不一定,但是通常都是结合使用。有一张图可供总结:
18、redis三种集群方式
主从复制
1、 什么是主从复制?
-
主从复制就是存在多个服务器,一台作为主服务器(master),多台作为从服务器(slave)
master服务器数据,会全部发送给slave服务器,上述称为主从复制
2、为什么要进行主从复制?
- 我们知道一个网站大部分的时候,服务器收到的都是请求读的操作,写的操作相对来说会少很多。如果我们用一台服务器 即支持读,又支持写,一旦出现高并发的情况,服务器很可能就凉凉。于是要进行读写分离。进行主从复制,将多台服务器作为读的服务器,一台作为写的服务器。master服务器主要负责写服务,slave服务器主要负责读服务,为保证数据的一直性,master服务器会将数据全部发给slave服务器
3、主从复制原理
和MySQL需要主从复制的原因一样,Redis虽然读取写入的速度都特别快,但是也会产生性能瓶颈,特别是在读压力上,为了分担压力,Redis支持主从复制。Redis的主从结构一主一从,一主多从或级联结构,复制类型可以根据是否是全量而分为全量同步和增量同步。
下图为级联结构:
4、全量复制
Redis全量复制一般发生在slave的初始阶段,这时slave需要将master上的数据都复制一份,具体步骤如下:
(1)、slave连接master,发送SYNC命令;
(2)、master街道SYNC命令后,执行BGSAVE命令生产RDB文件并使用缓冲区记录此后执行的所有写命令;
(3)、master的BGSAVE执行完成后,向所有的slave发送快照文件,并在发送过程中继续记录执行的写命令;
(4)、slave收到快照后,丢弃所有的旧数据,载入收到的数据;
(5)、master快照发送完成后就会开始向slave发送缓冲区的写命令;
(6)、slave完成对快照的载入,并开始接受命令请求,执行来自master缓冲区的写命令;
(7)、slave完成上面的数据初始化后就可以开始接受用户的读请求了。
5、增量复制
增量复制实际上就是在slave初始化完成后开始正常工作时master发生写操作同步到slave的过程。增量复制的过程主要是master每执行一个写命令就会向slave发送相同的写命令,slave接受并执行写命令,从而保持主从一致。
6、 测试集群
-
复制几份配置文件 分别是redis.conf redis6380.conf redis6381.conf
-
修改配置文件中的配置
port,pidfile,logfile,dbfileName
-
分别启动即可
-
查看是否启动成功
-
通过 slaveof ip port命令动态的设置为从服务器设置master (但不是永久的)
-
通过 info replication 查看当前服务器主从复制信息
-
从服务器是不能自己set key的哦,从服务器主要负责读服务
7、Redis主从同步的策略(全量复制和增量复制相结合)
主从同步刚连接的时候进行全量同步;全量同步结束后开始增量同步。如果有需要,slave在任何时候都可以发起全量同步,其主要策略就是无论如何首先会尝试进行增量同步,如果步成功,则会要求slave进行全量同步,之后再进行增量同步。
注意:如果多个slave同时断线需要重启的时候,因为只要slave启动,就会和master建立连接发送SYNC请求和主机全量同步,如果多个同时发送SYNC请求,可能导致master IO突增而发送宕机。
哨兵模式(有待补充和实验)
-
为什么要用哨兵模式?
- 当主从复制服务器群搭建好后,主服务器宕机,需要手动配置从服务器变为master服务器,而这一步需要手动,效率不高。使用哨兵模式,开启哨兵进程对主从服务器进行监控,一旦发现主服务器宕机,会进行投票选举,选举出一个新的slave服务器作为主服务器,而这一流程是自动的。
-
哨兵的功能
- 监控所有Redis节点(主从服务器)是否正常运行;
- master故障后可以通过投票机制,从slave中选举出新的master,保证集群正常运行。
在一个一主多从的集群中,可以启用多个哨兵进行监控以保证集群足够稳健,这种情况下,哨兵不仅监控主从服务,哨兵之间也会相互监控,建议哨兵至少3个并且是奇数。
-
哨兵的任务
- 监控:哨兵会不断的检测master和slave之间是否运行正常;
- 提醒:当监控的某个Redis出现问题,哨兵可以通过API向管理员或其他应用程序发送通知;
- 故障迁移:当一个master不能正常工作时,哨兵会开始一次自动故障迁移操作,它会将失效master的其中一个slave提升为master,并让失效master和其他slave该为复制新的master,当客户端试图连接失效的master时,集群也会向客户端返回新的master地址,使得集群可以使用新的master代替失效的master。
-
工作原理
哨兵是一个分布式系统,你可以在一个架构中运行多个哨兵(sentinel) 进程,这些进程使用流言协议来接收关于Master是否下线的信息,并使用投票协议来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master。
每个哨兵会向其它哨兵、master、slave定时发送消息,以确认对方是否”活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂。若“哨兵群”中的多数sentinel都报告某一master没响应,系统才认为该master"彻底死亡",通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置。
虽然哨兵释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis 服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵。 -
缺点
- 当主服务器宕机后,从服务器切换的那个时间,服务不可以用
使用方式
1、调整结构,6379带着6380、6381
2、新建sentinel.conf文件,名字绝不能错
3、配置哨兵,填写内容
- sentinel monitor 被监控数据库名字(自己起名字) 127.0.0.1 6379
- 上面最后一个数字1,表示主机挂掉后salve投票看让谁接替成为主机,得票数多少后成为主机(PS. 跟官网的描述有出入,下面有官方文档说明
4、启动哨兵
- redis-sentinel /sentinel.conf(上述目录依照各自的实际情况配置,可能目录不同)
5、正常主从演示
6、原有的master挂了
7、投票新选
8、重新主从继续开工,info replication查查看
主从复制+哨兵模式的缺点
复制延时
由于所有的写操作都是先在Master上操作,然后同步更新到slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
集群(有待实验)
Redis在3.0版本开始正式引用集群特性,Redis集群是一个分布式,高容错的内存K/V系统,集群可以使用的功能是普通单机Redis所使用的功能的一个子集,比如,Redis集群并不支持处理多个keys的命令,因为这需要在不同节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会出现无法预估的错误。
- Redis集群的特征
Redis集群有以下几个重要的特征:
(1)、Redis集群的分片特征在于将空间拆分为16384个槽位,某一个节点负责其中一些槽位;
(2)、Redis集群提供一定程度的可用性,可以在某个节点宕机或者不可达的情况继续处理命令;
(3)、Redis集群不存在中心节点或代理节点,集群的其中一个最重要的设计目标是达到线性可扩展性;
其架构如下:
其中每一个圆代表一个节点,任何两个节点是互通的,可以归纳以下几点:
- 所有的节点相互连接;
- 集群消息通信通过集群总线通信,,集群总线端口大小为客户端服务端口+10000,这个10000是固定值;
- 节点与节点之间通过二进制协议进行通信;
- 客户端和集群节点之间通信和通常一样,通过文本协议进行;
- 集群节点不会代理查询;
- Redis集群的原理
Redis Cluster中有一个16384长度的槽的概念,他们的编号为0、1、2、3……16382、16383。这个槽是一个虚拟的槽,并不是真正存在的。正常工作的时候,Redis Cluster中的每个Master节点都会负责一部分的槽,当有某个key被映射到某个Master负责的槽,那么这个Master负责为这个key提供服务,至于哪个Master节点负责哪个槽,这是可以由用户指定的,也可以在初始化的时候自动生成(redis-trib.rb脚本)。这里值得一提的是,在Redis Cluster中,只有Master才拥有槽的所有权,如果是某个Master的slave,这个slave只负责槽的使用,但是没有所有权。
那么Redis集群是怎么存储的呢?
首先,在redis的每一个节点上,都有这么两个东西,一个是插槽(slot)可以理解为是一个可以存储两个数值的一个变量这个变量的取值范围是:0-16383。还有一个就是cluster我个人把这个cluster理解为是一个集群管理的插件。当我们的存取的key到达的时候,redis会根据crc16的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。
还有就是因为如果集群的话,是有好多个redis一起工作的,那么,就需要这个集群不是那么容易挂掉,所以,理论上就应该给集群中的每个节点至少一个备用的redis服务,这个备用的redis称为从节点(slave)。然后,每一个节点都存有这个集群所有主节点以及从节点的信息,它们之间通过互相的ping-pong判断是否节点可以连接上。
如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点。如果某个节点和所有从节点全部挂掉,我们集群就进入faill状态。还有就是如果有一半以上的主节点宕机,那么我们集群同样进入发力了状态。这就是我们的redis的投票机制。
(1)、投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超时(cluster-node-timeout)认为当前master节点挂掉.
(2)、什么时候整个集群不可用(cluster_state:fail)
a、如果集群任意master挂掉,且当前master没有slave.集群进入fail状态,也可以理解成集群的slot映射[0-16383]不完整时进入fail状态. ps : redis-3.0.0.rc1加入cluster-require-full-coverage参数,默认关闭,打开集群兼容部分失败。
b、如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。
19、缓存穿透,缓存击穿,缓存雪崩(有待补充和实验)
-
缓存穿透(对于key,缓存,数据库都没有):key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
-
缓存击穿(key过期,绕过缓存直接访问数据库):key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
-
缓存雪崩(缓存中大量key集中失效):当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
1、缓存穿透解决方案
一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
粗暴方式伪代码:
//伪代码
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
String cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null) {
return cacheValue;
}
cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//数据库查询不到,为空
cacheValue = GetProductListFromDB();
if (cacheValue == null) {
//如果发现为空,设置个默认值,也缓存起来
cacheValue = string.Empty;
}
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
return cacheValue;
}
}
2、缓存击穿解决方案
key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
memcache代码:
if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}
3、缓存雪崩解决方案
与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key。
缓存正常从Redis中获取,示意图如下:
缓存失效瞬间示意图如下:
缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
加锁排队,伪代码如下:
//伪代码
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
String lockKey = cacheKey;
String cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
synchronized(lockKey) {
cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//这里一般是sql查询数据
cacheValue = GetProductListFromDB();
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
}
}
return cacheValue;
}
}
加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!
注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!
随机值伪代码:
//伪代码
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
//缓存标记
String cacheSign = cacheKey + "_sign";
String sign = CacheHelper.Get(cacheSign);
//获取缓存值
String cacheValue = CacheHelper.Get(cacheKey);
if (sign != null) {
return cacheValue; //未过期,直接返回
} else {
CacheHelper.Add(cacheSign, "1", cacheTime);
ThreadPool.QueueUserWorkItem((arg) -> {
//这里一般是 sql查询数据
cacheValue = GetProductListFromDB();
//日期设缓存时间的2倍,用于脏读
CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);
});
return cacheValue;
}
}
解释说明:
- 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
- 缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。
关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间,还有一种被称为“二级缓存”的解决方法。
20、springboot集成redis (使用注解)
注意:一般开发中小型快速应用,适合redis注解开发。但是想要合理点的设置缓存,建议还是手动配置
为什么要使用注解版?
-
注解版使用方便
-
注解版功能多样化,适合多种环境
哪种不适合缓存
-
insert插入数据库后,返回一个int值,这个值有必要缓存???
没必要。因为一般情况下我不会从缓存中取出int值,例如我插入了一个数据,缓存一个int值,在再插入一个数据,这种缓存一般不会被使用。
而且插入一条数据后,就应该让该命名空间下的所有key全部移除,所以一般写的是@CacheEvict(value allEntries=true)
-
@EnableCache 开启基于注解的缓存功能
-
@Cacheable注解 :先从redis数据库中 按照当前key查找,有没有。如果redis中有,是不会走当前该方法的,如果没有再调用方法返回结果,如果结果不为null将其缓存到数据库中(一般用于find)
-
属性:
-
value:key的一部分(前缀),主要是指明数据放在那个key范围
-
key:key的主体,#p0:指明取出第一个参数 #p1:指明取出第二个参数。。。依此类推
-
unless:结果为true,将当前的数据结果不保存到redis,#result:指明取出数据库中返回的结果
-
condition 结果如果为true,将当前数据保存到redis
-
实例
@Cacheable(value = RedisConfig.REDIS_DATABASE_KEY, key = "'user-'+#p0", unless = "#result==null") @Override public User getUserByName(String name) { UserExample example = new UserExample(); example.createCriteria().andNameEqualTo(name); List<User> users = userMapper.selectByExample(example); if(users.size()==0){ return null; } return users.get(0); }
-
-
@CachePut: 主要用于向数据库中插入数据,向数据中插入数据的时候,会将返回的int类型,放入redis中缓存,当然是有选择性的(一般用于insert)
-
属性:
-
value:key的一部分,命名空间
-
key:指定key的名称
-
unless:满足条件,则不将返回的结果放入redis
-
condition: 满足条件,则将返回的结果放入redis
-
实例
@CachePut(value = RedisConfig.REDIS_DATABASE_KEY,key = "'user-insert-'+#p0.id",unless = "#result==0") @Override public int insert(User record) { return userMapper.insert(record); }
-
@CacheEvict:满足条件则移除当前key在redis中的数据(一般用于update/delete)
-
属性:
-
value: 同理命名空间
-
key: key名称
-
condition:满足什么条件从缓存中移除指定的key
-
AllEntries:true/false 是否移除命名空间下的所有key
-
实例
@CacheEvict(value = RedisConfig.REDIS_DATABASE_KEY,key = "'user-'+#p0.id",condition = "#result==1") @Override public int updateByPrimaryKey(User record) { return userMapper.updateByPrimaryKey(record); }
-
-
问题
我们知道如果进行查询的时候,会将数据缓存到redis中。一旦进行增删改,那么原本数据库中的数据可能会发生变化,那么增删改成功后,应该要指定的移除指定key的缓存。但是我们通过@CaheEvict移除指定key的数据,发现并不能行的通。
为什么?
例如:我们修改了一个product信息,那么应该移除product-id 这种key的数据,但是如果这个product数据关联,product-list等等这种key(即list中有当前修改前的数据),按理说我们应该移除这些相关联的key
怎么做
-
第一种方法
@CaheEvict(value=“nameSpace”,allEntries=true),移除这个命名空间下的所有数据
@CaheEvict(value=“product”,allEntries=true),移除product命名空间下的所有keys
-
第二种
通过redisTemplate手动移除相关联的key的数据(相对来说麻烦点)
-
-
完整配置
-
pom.xml
<!--springboot中的redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- lettuce pool 缓存连接池--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
-
redisConfig
//开启基于注解的配置 @EnableCaching @Configuration public class RedisConfig { /** * redis数据库自定义key的命名空间 */ public static final String REDIS_DATABASE_KEY="tmall_springboot"; public static final String REDIS_CATEGORY_KEY="category"; public static final String REDIS_ORDER_ITEM_KEY="orderItem"; public static final String REDIS_ORDER_KEY="order"; public static final String REDIS_PRODUCT_KEY="product"; public static final String REDIS_PROPERTY_KEY="property"; public static final String REDIS_PROPERTY_VALUE_KEY="propertyValue"; public static final String REDIS_REVIEW_KEY="review"; public static final String REDIS_USER_KEY="user"; public static final String REDIS_PRODUCT_IMAGE_KEY="productImage"; /** * 自动配置的redisTemplate,存在序列化问题,会导致存入redis数据库中的数据,不容易看清所以需要自己配置 */ /** * 配置自定义redisTemplate * @return */ @Bean RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory,RedisSerializer redisSerializer) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置键(key)的序列化采用StringRedisSerializer。 redisTemplate.setKeySerializer(new StringRedisSerializer()); // 设置值(value)的序列化采用Jackson2JsonRedisSerializer。 redisTemplate.setValueSerializer(redisSerializer); // 设置hashKey的序列化 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(redisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } /** * 配置json序列化器(我们使用jackson的序列化器) */ @Bean public RedisSerializer redisSerializer(){ 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); return jackson2JsonRedisSerializer; } /** * 配置redis缓存管理器,管理注解版的缓存 */ @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory,RedisSerializer redisSerializer) { RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); //设置Redis缓存有效期为10分钟 RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) //ttl .entryTtl(Duration.ofHours(2)); return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration); } }
-
application.yml
# 配置redis redis: host: 115.29.111.155 port: 6379 database: 0 #默认使用0号数据库 timeout: 2000 #设置连接超时时间 lettuce: pool: max-idle: 10
-
service层
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @CacheEvict(value = RedisConfig.REDIS_USER_KEY,allEntries = true,condition = "#result==1") @Override public int deleteByPrimaryKey(Integer id) { return userMapper.deleteByPrimaryKey(id); } /** * @CachePut: 主要用户向数据库中插入数据,向数据中插入数据的时候,会将返回的int类型,放入redis中缓存,当然是有选择性的 * 属性: * value:key的一部分,命名空间 * key:指定key的名称 * unless:满足条件,则不将返回的结果放入redis * condition: 满足条件,则将返回的结果放入redis */ // @CachePut(value = RedisConfig.REDIS_USER_KEY,key = "'insert-'+#p0.id",unless = "#result==0") @CacheEvict(value = RedisConfig.REDIS_USER_KEY,allEntries = true,condition = "#result==1") @Override public int insert(User record) { return userMapper.insert(record); } // @CachePut(value = RedisConfig.REDIS_USER_KEY,key = "'insert-'+#p0.id",unless = "#result==0") @CacheEvict(value = RedisConfig.REDIS_USER_KEY,allEntries = true,condition = "#result==1") @Override public int insertSelective(User record) { return userMapper.insertSelective(record); } @Cacheable(value = RedisConfig.REDIS_USER_KEY,key = "'list'",unless = "#result==null") @Override public List<User> selectByExample() { UserExample example = new UserExample(); example.setOrderByClause("id desc"); List<User> users = userMapper.selectByExample(example); return users; } /** * 查找数据库是否存在与该名称相同的用户,如果存在true else false * * 我们这样规定,对于key而言,返回boolean的加一级 exist * */ @Cacheable(value = RedisConfig.REDIS_USER_KEY , key = "'exist-'+#p0") @Override public boolean findUserByName(String name) { UserExample example = new UserExample(); example.createCriteria().andNameEqualTo(name); List<User> users = userMapper.selectByExample(example); if(users.size()==0){ return false; } return true; } /** * 通过username获取用户,username是唯一的 * redis中使用缓存缓存数据。 * * @Cacheable 开启基于注解的缓存功能 * * @Cacheable注解 :先从redis数据库中 按照当前key查找,有没有。如果没有再调用方法返回结果,如果结果不为null将其缓存到数据库中 * 属性: * value:key的一部分(前缀),主要是指明数据放在那个key范围 * key:key的主体,#p0:指明取出第一个参数 #p1:指明取出第二个参数。。。依此类推 * unless:结果为true,将当前的数据结果不保存到redis,#result:指明取出数据库中返回的结果 * condition 结果如果为true,将当前数据保存到redis * 此为生成的key,中文再redis中显示是这样的 * "tmall_springboot::user-\xe5\xbc\xa0\xe4\xb8\x89" */ @Cacheable(value = RedisConfig.REDIS_USER_KEY, key = "'one-name-'+#p0", unless = "#result==null") @Override public User getUserByName(String name) { // System.out.println("走该方法,数据应该缓冲"); UserExample example = new UserExample(); example.createCriteria().andNameEqualTo(name); List<User> users = userMapper.selectByExample(example); if(users.size()==0){ return null; } return users.get(0); } /** * 通过userName,password,查询是否有此用户 */ @Cacheable(value = RedisConfig.REDIS_USER_KEY,key = "'one-'+#p0.id",unless = "#result==null") @Override public User findOneByNameAndPassword(User user){ UserExample example = new UserExample(); example.createCriteria().andNameEqualTo(user.getName()).andPasswordEqualTo(user.getPassword()); List<User> users = userMapper.selectByExample(example); if(users.size()==1){ return users.get(0); } return null; } @Cacheable(value = RedisConfig.REDIS_USER_KEY , key = "'one-'+#p0",unless = "#result==null") @Override public User selectByPrimaryKey(Integer id) { return userMapper.selectByPrimaryKey(id); } /** * @CacheEvict:满足条件则移除当前key在redis中的数据 * 属性: * value: 同理命名空间 * key: key名称 * condition:满足什么条件缓存到redis中 * allEntries: 移除value(命名空间)下,全部缓存 */ @CacheEvict(value = RedisConfig.REDIS_USER_KEY,allEntries = true,condition = "#result==1") @Override public int updateByPrimaryKeySelective(User record) { return userMapper.updateByPrimaryKeySelective(record); } @CacheEvict(value = RedisConfig.REDIS_USER_KEY,allEntries = true,condition = "#result==1") @Override public int updateByPrimaryKey(User record) { return userMapper.updateByPrimaryKey(record); } }
-
21、redis @Cacheable/@CacheEvict注意事项
1、一个类中,方法A调用 标志了@Cahcheable的方法B,缓存会失效,好像是Spring AOP动态代理的问题,对内部方法不进行代理,因此如果使用缓存redis注解不要使用this调用 标注了@Cahcheable的本类方法B。
2、@Cacheable方法中发生异常,缓存保存失败
3、一个标志@Cacheable方法,常常会调用其它类中的@Cacheable方法。
例如:article类 方法A,调用comment类 方法B,A(article)->B(comment) 。第一次缓存访问article A()时,会对A , B 方法都缓存。
当B(comment)对应的数据发生改变时,如果是增加一个comment,会移除comment在redis中的所有数据。
当我们再次访问 A()时,会看缓存中是否有该缓存,结果是有的,因为article对应数据未发生改变。但是article对应的comment数据此时已经发生改变了,那么当前访问A()获取的数据只是缓存中的错误数据。
因此:当子表comment数据发生改变时,不仅应该移除 comment在redis中的缓存,还应该移除父表article在redis中的缓存。
22、redis 日志
redis在默认情况下,是不会生成日志文件的,所以需要配置
配置方法:
1、首先找到redis的配置文件
2、打开配置文件,找到logfile(可能有多个logfile,认准旁边有loglevel的那个),或者直接搜logfile “”
3、将路径填入logfile后面的引号内,例如:logfile “d:/redislog/redis.log” (注意斜杆的方向,这个和windows cmd中的斜杆方向是反的)
4、根据自己写的路径,手动将日志文件夹建好,日志文件不用建,建到文件夹即可,比如我就手动建立了d:\redislog 文件夹
5、保存配置文件,以这个配置文件启动redis,然后这时候redis的启动框会变成一个黑框框,什么输出都没有,这就对了(因为输入全写到日志文件去了)
然后就可以去d:\redislog\redis.log文件夹去查看日志了
其他注意事项:
1、redis必须带配置文件启动,如果直接启动的话,它会使用默认配置(而且并不存在这个默认配置文件,所以不要想改它)。
2、如果出现
说明没有指定配置文件或者配置文件读取不到(路径错误)
3、loglevel是用来设置日志等级的,具体可以看配置文件中上面的注释
23、window下后台运行redis
-
在进入redis的安装目录
-
输入:redis-server --service-install redis.windows.conf --loglevel verbose ( 安装redis服务 )
-
输入:redis-server --service-start ( 启动服务 )
-
输入:redis-server --service-stop (停止服务)