Redis
一、简介
Redis(mote ctionary erver),也就是说,远程字典服务是一个支持网络,可以基于内存或可持续的开源日志类型Key-Value。
NoSQL最常见的解释是non-relational
,Not Only SQL
。大规模高并发SNS类型的web2.0纯动态网站已显得力不从心,出现了许多难以克服的问题,NoSQL数据库的产生是为了解决大规模数据型的挑战,特别是大数据应用问题。
NoSql数据库分类:
二、Linux环境下安装Redis
下载链接:https://github.com/redis/redis/archive/7.0.0.tar.gz
上传到Linux环境解压:
安装gcc:
安装所需的环境:
确认安装完成:
进入/usr/local/bin目录查看:
将redis.conf文件复制到/usr/local/bin目录下:
修改配置文件redis.conf,将其更改为后台启动:
通过指定的配置文件启动redis服务:
使用客户端测试连接:
查看redis进程:
关机命令:shutdown
三、性能测试
本机测试100连接数10000请求数:
参数列表:
四、基本命令
1.type
type 返回命令 key 存储值的类型。
2.keys
keys 命令用于找到符合给定模式的所有命令 pattern 的 key 。
keys *
在数据库下搜索一切key值:
3.flushdb/flushall
flushdb
清空命令数据库的数据。
flushall
清空命令数据(删除所有数据库中的所有数据 key )。
4.rename
rename 修改命令 key 的名称 。
5.renamenx
renamenx 新的命令 key 不存在时修改 key 的名称 。
修改成功时,返回 1 。 如果新名已经存在,返回 0 。
6.exists
exists 命令用于检查给定 key 是否存在。
7.del
del 命令用于删除现有键。删除成功返回1,删除不存在的键 key返回0 。
8.select
select 命令用于切换到指定的数据库,数据库索引号 index 指定数字值,查看redis.conf文件,redis默认数据库数量为16,下标为0~15。
9.move
move 使用当前数据库的命令 key 移动到给定的数据库 db 当中。
10.randomkey
randomkey 命令从当前数据库中随机返回 key 。
当数据库不空时,返回一个 key 。 当数据库空时,返回 nil 。
11.expire/ttl
expire 设置命令 key 过期时间。key 过期后将不再可用。
设置成功返回 1 ,当 key 不存在返回 0 。
ttl 命令以秒为单位返回 key 剩余过期时间。
当 key 不存在时,返回 -2 。 当 key 当存在但未设定剩余生存时间时,返回 -1 。 否则,以毫秒为单位返回 key 剩余的生存时间。
12.persist
persist 命令用于删除给定 key 过期时间,使 key 永不过期。
当过期时间成功移除时,返回 1 。 如果 key 不存在或 key 过期时间没有设置,返回 0 。
五、Redis 数据类型
1.String(Key-Value)
set 命令用于设置给定 key 的值。如果 key 其他值已存储, set 复制旧值,忽略类型。
get 命令用于获取指定 key 的值。如果 key 不存在,返回 nil 。如果key 储存的值不是字符串类型,返回一个错误。
- :
setnx( if ot eists) 命令在指定的 key 不存在时,为 key 设置指定值。
设置成功,返回 1 。 设置失败,返回 0 。
- :
setex 命令为指定的 key 假设值及其过期时间。 key 存在, setex 命令将取代旧值。
- :
mset 命令用于同时设置一个或多个 key-value 对。
mget 命令返回所有(一个或多个) key 的值。 如果给定的 key 里面,有一个 key 不存在,所以这个 key 返回特殊值 nil 。
incr 命令将 key 中存储的数字值增加,decr 命令将 key 中存储的数字值减一。
如果 key 不存在,所以 key 的值会先被 ,然后再执行 INCR 操作。
如果值包含错误的类型,或者字符串类型的值不能表示为数字,则返回错误。
操作值限制在 64 位(bit)有符号数字表示。
- :
incrby 命令将 key 中间存储的数字加上指定的增量值, decrby 命令将 key 存储值减去指定的减量值。
- :
strlen 命令用于获取指定 key 存储的字符串值的长度。
当 key 当不存储字符串值时,返回一个错误。 当 key 不存在时,返回 0。
- :
setrange 命令用指定的字符串覆盖 key 存储的字符串值从偏移量覆盖的位置 offset 开始。
getrange 该命令用于获取指定的存储 key 中间字符串的子字符串。字符串的截取范围由字符串截取 start 和 end 两个偏移决定(包括两个偏移决定(包括) start 和 end 在内)。
2.Hash(Key-Map)
hset/hget :
hset 命令用于为哈希表中的字段赋值 。
如果哈希表不存在,一个新的哈希表被创建并进行 hset 操作。
如果字段已经存在于哈希表中,旧值将被覆盖。
hget 命令用于返回哈希表中指定字段的值。
hmset/hmget :
hmset 命令用于同时将多个 field-value (字段-值)对设置到哈希表中。
hmget 命令用于返回哈希表中,一个或多个给定字段的值。
hsetnx :
hsetnx 命令用于为哈希表中不存在的的字段赋值 。
如果哈希表不存在,一个新的哈希表被创建并进行 hset 操作。
如果字段已经
hexists :
hexists 命令用于查看哈希表的指定字段是否存在。
如果哈希表含有给定字段,返回 1 。 如果哈希表不含有给定字段,或 key 不存在,返回 0 。
hincrby :
hincrby 命令用于为哈希表中的字段值加上指定增量值。
增量也可以为
如果哈希表的 key 不存在,一个新的哈希表被创建并执行 hincrby 命令。
如果指定的字段不存在,那么在执行命令前,字段的值被初始化为 0 。
对一个储存字符串值的字段执行 hincrby 命令将造成一个错误。
本操作的值被限制在 64 位(bit)有符号数字表示之内。
hlen :
hlen 命令用于获取哈希表中字段的数量。
当 key 不存在时,返回 0 。
hdel :
hdel 命令用于删除哈希表 key 中的一个或多个指定字段,不存在的字段将被忽略。
hgetall/hkeys/hvals :
hgetall 命令用于返回哈希表中,所有的字段和值。
hkeys 命令用于获取哈希表中的所有字段名,当 key 不存在时,返回一个空列表。
hvals 命令返回哈希表所有字段的值,当 key 不存在时,返回一个空表。
3.List
Lpush/Rpush :
Lpush 命令将一个或多个值插入到列表
如果 key 不存在,一个空列表会被创建并执行 Lpush 操作。
当 key 存在但不是列表类型时,返回一个错误。
Lpop/Rpop :
Lpop 命令用于从列表头部移除一个或多个元素并返回移除的元素。
Rpop 命令用于从列表尾部移除一个或多个元素并返回移除的元素。
Lrange :
Lrange 返回列表中指定区间内的元素,区间以偏移量 start 和 end 指定。
其中 0 表示列表的第一个元素,也可以使用负数下标,以 -1 表示列表的最后一个元素。
Llen :
Llen 命令用于返回列表的长度。
如果列表 key 不存在,则返回 0 。 如果 key 不是列表类型,返回一个错误。
Lindex :
Lindex 命令用于通过索引获取列表中的元素。
也可以使用负数下标,以 -1 表示列表的最后一个元素。
Lset :
Lset 通过索引来设置元素的值。
当索引参数超出范围,或对一个空列表进行 LSET 时,返回一个错误。
Lrem :
Lrem 根据参数 count 的值,移除列表中与参数 value 相等的元素。
count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。
count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。
count = 0 : 移除表中
Linsert :
Linsert 命令用于在列表的元素前或者后插入元素。
当指定元素不存在于列表中时,不执行任何操作。
当列表不存在时,被视为空列表,不执行任何操作。
如果 key 不是列表类型,返回一个错误。
Ltrim :
Ltrim 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
Rpoplpush :
Rpoplpush 命令用于移除列表的最后一个元素,并将该元素添加到另一个列表并返回。
4.Set
Sadd :
Sadd 命令将一个或多个成员元素加入到集合中,
假如集合 key 不存在,则创建一个只包含添加的元素作成员的集合。
当集合 key 不是集合类型时,返回一个错误。
Smembers :
Smembers 命令返回集合中的所有的成员。 不存在的集合 key 被视为空集合。
Spop :
Spop 命令用于移除并返回集合中的一个或多个
Srem :
Srem 命令用于移除集合中的一个或多个
Scard :
Scard 命令返回集合中元素的数量。当集合 key 不存在时,返回 0 。
Sismember :
Sismember 命令判断成员元素是否是集合的成员。
如果成员元素是集合的成员,返回 1 。 如果成员元素不是集合的成员,或 key 不存在,返回 0
Sunion :
Sunion 命令返回给定集合的
Sdiff :
Sdiff 命令返回给定集合之间的
Sinter :
Sinter 命令返回给定所有给定集合的
结果也为空集。
Smove :
Smove 命令将指定成员 member 元素从 source 集合移动到 destination 集合,是
如果 source 集合不存在或不包含指定的 member 元素,则 Smove 命令不执行任何操作,仅返回 0 。否
则, member 元素从 source 集合中被移除,并添加到 destination 集合中去。当 destination 集合已经包含
member 元素时, Smove 命令只是简单地将 source 集合中的 member 元素删除。当 source 或 destination
不是集合类型时,返回一个错误。
5.Zset(sorted set)
Zadd :
Zadd 命令用于将一个或多个成员元素及其值加入到有序集当中。
如果某个成员已经是有序集的成员,那么
在正确的位置上。分数值可以是整数值或双精度浮点数。如果有序集合 key 不存在,则创建一个空的有序集
并执行 ZADD 操作。当 key 存在但不是有序集类型时,返回一个错误。
Zrange :
Zrange 返回有序集中,指定区间内的成员。
其中成员的位置按分数值递增(
具有
以 0 表示有序集第一个成员,以 -1 表示最后一个成员
加上rev 参数颠倒顺序,加上withscores带值显示
Zrevrange :
Zrevrange 命令返回有序集中,指定区间内的成员。
其中成员的位置按分数值递减(
ZrangeByscore :
Zrangebyscore 返回有序集合中指定分数区间的成员列表。
有序集成员按分数值递增(
默认情况下,区间的取值使用闭区间,通过给参数前增加 (
变成开区间
ZrevrangeByscore :
Zrevrangebyscore 返回有序集中指定分数区间内的所有的成员。
有序集成员按分数值递减(
Zcount :
Zcount 命令用于计算有序集合中指定分数区间的成员
Zcard :
Zcard 命令用于计算集合中元素的
Zrem :
Zrem 命令用于移除有序集中的一个或多个成员,不存在的成员将被忽略。
当 key 存在但不是有序集类型时,返回一个错误。
Zscore :
Zscore 命令返回有序集中,成员的分数值。
如果成员元素不是有序集 key 的成员,或 key 不存在,返回 nil 。
Zrank :
Zrank 返回有序集中指定成员的下标。
ZincrBy :
Zincrby 命令对有序集合中指定成员的分数加上增量 increment
可以通过传递一个负数值 increment ,让分数减去相应的值
当 key 不存在,或分数不是 key 的成员时等同于添加一个元素
6.Geospatial(地理位置)
Geoadd :
将指定的地理空间位置(纬度、经度、名称)添加到指定的key
中,这些数据将会存储到sorted set
,实质上
为Zset类型,有效的经度从-180度到180度。有效的纬度从-85.05112878度到85.05112878度。当坐标位置超
出上述指定范围时,该命令将会返回一个错误。
Geopos :
从key
里返回所有给定位置元素的位置(经度和纬度)。
Geodist :
返回两个给定位置之间的距离
如果两个位置之间的其中一个不存在, 那么命令返回空值。
如果用户没有显式地指定单位参数,默认使用米作为单位。
m 表示单位为米。km 表示单位为千米。mi 表示单位为英里。ft 表示单位为英尺。
Geodist
命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。
GeoRadiusByMember :
以给定的
withdist
: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。withcoord
: 将位置元素的经度和维度也一并返回。
命令默认返回
通过以下两个参数, 用户可以指定被返回位置元素的排序方式:
asc
: 根据中心的位置, 按照从近到远的方式返回位置元素。desc
: 根据中心的位置, 按照从远到近的方式返回位置元素。
7.hyperloglogs(基数统计)
Redis HyperLogLog 是用来做基数(
HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的,并且
是很小的。每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。
pfadd :
pfadd 命令将所有元素参数添加到 HyperLogLog 数据结构中。
如果至少有一个元素被添加返回 1, 否则返回 0。
pfmerge :
pfmerge 命令将多个 HyperLogLog 合并为一个 HyperLogLog ,合并后的 HyperLogLog 的基数估算值是通过对所有 给定 HyperLogLog 进行并集计算得出的。
pfcount :
pfcount 命令返回给定 HyperLogLog 的基数估算值。
如果多个 HyperLogLog 则返回基数估值之和。
8.Bitmap(位图)
操作二进制0、1进行计数
setbit :
设置字符串某一位的值
bitcount :
统计字符中每一位上值为1的个数
六、Redis事务和乐观锁
Multi :
Multi 命令用于标记一个事务块的开始。
事务块内的多条命令会按照先后顺序被放进一个exec
命令
Exec :
Exec 命令用于执行所有事务块内的命令。当操作被打断时,返回空值 nil 。
Discard :
Discard 命令用于取消事务,放弃执行事务块内的所有命令。
Redis没有隔离级别
Redis
Redis乐观锁使用Watch实现:
Watch :
Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务
将被打断
Unwatch :
Unwatch 命令用于取消 WATCH 命令对所有 key 的监视。
使用两个客户端连接redis
未使用乐观锁时:
使用乐观锁Watch
:
检测money被其他命令所改动,事务将被打断
七、SpringBoot整合
导入starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置:
spring:
redis:
host: localhost # 主机默认为本机
port: 6379 # 默认端口为6379
#password: 默认为空
#client-type: jedis 设置客户端类型 默认为letture
1.Jedis
全,但函数与Redis命令一一对应方便理解
导入依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.2.2</version>
</dependency>
@SpringBootTest
public class JedisTest {
@Test
void test(){
//连接Redis
Jedis jedis = new Jedis("127.0.0.1", 6379);
//jedis.auth("Your Password"); //检验Redis密码
jedis.set("a", "1");
System.out.println(jedis.get("a"));
//关闭连接
jedis.close();
}
}
2.Letture
@SpringBootTest
public class RedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
void test(){
Group group = new Group("1", 10, "2020-1-1");
redisTemplate.opsForValue().set("姓名", "张三");
redisTemplate.opsForValue().set("性别", "男");
redisTemplate.opsForValue().set("年龄", 18);
redisTemplate.opsForValue().set("group", group);
System.out.println(redisTemplate.opsForValue().get("姓名"));
System.out.println(redisTemplate.opsForValue().get("性别"));
System.out.println(redisTemplate.opsForValue().get("年龄"));
System.out.println(redisTemplate.opsForValue().get("group"));
}
}
/* 张三 男 18 Group(name=1, userNum=10, time=2020-1-1) */
redis数据进制问题:
使用redis-cli --raw
命令连接查看:
产生中文乱码,由于redisTemplate将序列化后的值存入redis中造成
解决方法:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//方法已过时
//objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(objectJackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
八、持久化
1.RDB(Redis DataBase)
在指定时间间隔内将内存中的数据写入磁盘中,即Snapshot快照
Redis会(fork)创建一个子进程来进行持久化,将数据写入一个临时文件(dump.rdb),待所有数据都写入后,将
这个文件替换上一次的文件。在此过程中,主进程不进行任何IO操作,保证了极高的性能。但是,如果持久
化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。Redis
默认存储文件名
触发RDB持久化条件:
- save规则满足
- 执行flushall命令
- 退出redis(shutdown)
2.AOF(Append Only File)
将Redis执行过程中每一个写操作记录下来,每次进行追加,Redis启动会读取该文件(appendonly.aof)重新构
建数据,即将每一个写操作执行一遍。
测试aof:
aof文件破坏后无法连接redis:
可以通过redis-check-aof
进行修复,可能会丢失少量数据:
AOF存储的文件远大于RDB,但修复速度比RDB慢,运行效率也相对低一些
九、发布与订阅
Subscribe :
Subscribe 命令用于订阅给定的一个或多个频道的信息。
Publish :
Publish 命令用于将信息发送到指定的频道。
十、主从复制,读写分离
指一台Redis服务器上的数据复制到其他的Redis服务器上,前者称为
- 数据冗余
- 故障恢复
- 负载均衡
- 高可用
一般Redis运用中,只使用一台Redis是不行的:
1.单个Redis服务器会发生
2.单个Redis服务器内存容量有限,一般最大使用内存不应该超过20G
默认每一台Redis服务器都是
主节点。
使用info replication
查看节点信息:
搭建Redis伪集群,一个主节点加两个从节点:
复制三份redis配置文件:
修改配置文件:
如果主机配置了密码,需要修改masterauth password
运行Redis服务:
设置主节点:
也可以在配置文件中设置:
主节点:
主节点负责写入数据,从节点负责读取数据
主节点断开后,从节点依旧连接主节点但是无法进行写操作:
主机恢复后,依旧为主节点,写入的数据从节点也能获取到
从节点连接到主节点时会发送一个
后将这个文件发送到从节点完成一次同步。
全量复制
:从节点接受数据库文件后,将数据加载进内存中
增量复制
:主节点将新的写入操作依次发送给从节点
当从节点重新连接到主机节点时,发生一次全量复制。
十一、哨兵模式
通过发送心跳命令监控节点是否正常运行,当主节点宕机后,选取其从节点变成新的主节点,保
证Redis的高可用
新建一个哨兵配置文件sentinel.conf
:
# 哨兵监控本机6379端口
# 1代表只有一个以上的哨兵认为主服务器不可用的时候,才会进行failover操作(一般设置为哨兵数量的一半加一)
sentinel monitor master 127.0.0.1 6379 1
sentinel auth-pass master YourPassword # 如果节点设置了密码需要配置
主节点宕机后,选举出新的主节点
当进行failover操作后,配置文件会自动修改并记录信息
# 哨兵监听的端口变为6380,即当前的主节点
sentinel monitor master 127.0.0.1 6380 1
sentinel auth-pass master YourPassword
# Generated by CONFIG REWRITE
latency-tracking-info-percentiles 50 99 99.9
dir "/usr/local/bin"
protected-mode no
port 26379
user default on nopass sanitize-payload ~* &* +@all
sentinel myid 2bd96303e14b6e137d479c0dc04d53aec4edd303
sentinel config-epoch master 2
sentinel leader-epoch master 2
sentinel current-epoch 2
# 原来的主节点变成从节点
sentinel known-replica master 127.0.0.1 6379
sentinel known-replica master 127.0.0.1 6381
十二、Cluster集群
主从集群可以解决该并发读的问题,但是没有解决大量数据存储,由于只有主节点能进行写操作,并发写的
问题没有解决,这时需要使用cluster集群。
cluster集群中有多个master保存不同的数据,每个master有多个slave,master之间使用ping检测健康状态,
客户端可以访问任意节点,最后都会被转发到正确的节点上。所有主节点一起划分16384个插槽,数据存放
在插槽中,随着插槽移动。
搭建单节点cluster集群:
redis配置文件,每台redis只需将文件中所有的7000修改为对应的端口号:
bind 0.0.0.0
# 端口
port 7000
# 后台启动
daemonize yes
# 设置数据库个数为1
databases 1
# 关闭保护模式,让外部redis客户端访问
protected-mode no
pidfile /var/run/redis_7000.pid
# 日志存储目录及日志文件名
logfile "7000.log"
# rdb数据文件名
dbfilename "dump7000.rdb"
# rdb数据文件和aof数据文件的存储目录
dir "/usr/local/bin/cluster"
# 是否开启aof
appendonly no
# 设置密码
requirepass 密码
# 从节点访问主节点密码(必须与 requirepass 一致)
masterauth 密码
# 是否开启集群模式,默认 no
cluster-enabled yes
# 集群节点信息文件,会保存在 dir 配置对应目录下
cluster-config-file nodes-7000.conf
# 集群节点连接超时时间
cluster-node-timeout 15000
#绑定ip 和 端口号
cluster-announce-ip 外网ip #!!!注意 声明节点ip为云服务器外网ip
cluster-announce-port 7000
依次启动redis:
printf '%s\n' 7000 7001 7002 7003 7004 7005 | xargs -I{
} -t redis-server conf/cluster/{
}/redis.conf
依次关闭redis:
printf '%s\n' 7000 7001 7002 7003 7004 7005 | xargs -I{
} -t redis-cli -a 密码 -p {
} shutdown
创建cluster集群:
redis-cli -a 密码 --cluster create --cluster-replicas 1 外网IP:7000 外网IP:7001 外网IP:7002 外网IP:7003 外网IP:7004 外网IP:7005
在设置值和获取值的时候会自动转发到指定的节点上
springboot操作cluster集群:
spring:
redis:
password: 密码
client-type: lettuce
# cluster集群配置
cluster:
nodes: #指定分片集群地址
- 服务器Ip:7000
- 服务器Ip:7001
- 服务器Ip:7002
- 服务器Ip:7003
- 服务器Ip:7004
- 服务器Ip:7005
@RequestMapping("/redis")
@RestController
public class RedisController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/set")
public String set(String key, String value){
if(!StringUtils.hasLength(key) || !StringUtils.hasLength(value))
{
return "请输入key和value!!!";
}
stringRedisTemplate.opsForValue().set(key, value);
return "success";
}
@GetMapping("/get")
public String get(String key){
if(!StringUtils.hasLength(key))
{
return "请输入key!!!";
}
String s = stringRedisTemplate.opsForValue().get(key);
return "key: " + key + ", value: " + s;
}
}
集群伸缩:
十三、数据一致性
保证redis缓存内容和mysql数据库信息一致
1.更新缓存与删除缓存哪种方式更合适?
-
更新缓存:
优点:每次数据变化都及时更新缓存,所以查询时不容易出现未命中的情况。
缺点:更新缓存的消耗比较大,影响服务器的性能。如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取该数据。
-
删除缓存
优点:操作简单,无论更新操作是否复杂,都是将缓存中的数据直接删除。
缺点:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库。
从上面的比较来看,
2.先操作数据库还是缓存?
先删除缓存再更新数据库: