NoSQL
NoSQL = Not Only SQL
泛指非关系型数据库
NoSQL 特点
1、方便扩展(数据之间没有关系,很好扩展)
2、大数据量高性能(NoSQL 的缓存记录级,是一种细粒度的缓存,性能会比较高)
3、数据类型是多样型的(不需要事先设计数据库。随取随用,)
4、传统RDBMS 和 NoSQL
传统的 RDBMS
- 结构化组织
- SQL
- 数据和关系都存在单独的表中 row、col
- 数据操作语言、数据的定义语言
- 严格的一致性
- 基础的事务
- ....
NoSQL
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储、列存储、文档存储、图形数据库
- 最终一致性
- CAP定理和BASE理论
- 高可用、高性能、高可扩展
- ....
KV键值对:
文档型数据库(bson格式 和json一样)
列存储数据库
图关系数据库
Redis是什么
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的API。是NoSQL技术之一,也被称为结构化数据库!
redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis能干嘛
1、内存存储,持久化,内存中是断电即失,所以说持久化很重要(rdb、aof)
2、效率高,可以用于高速缓存
3、发布订阅系统
4、地图信息分析
5、计时器、计数器(浏览量)
redis-benchmark
#测试:100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
redis默认有16个数据库
默认使用的是第0个
我们可以使用 select 进行切换数据库
127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]> dbsize #查看db大小
(integer) 0
127.0.0.1:6379[3]>
清除数据库
127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]> keys *
(empty array)
redis是单线程还是多线程,为什么
redis4.0 之前是单线程运行的;redis4.0 后开始支持多线程。
redis4.0 之前使用单线程是的原因:
redis在4.0以及之后的版本中引入了惰性删除(也叫异步删除),意思是我们可以使用异步的方式对redis中的数据进行删除。这样处理的好处是不会使redis的主线程卡顿,会把这些操作交给后台线程来执行。
考点(面试题)
1、redis主线程既然是单线程,为什么还这么快?
1、基于内存操作,所有的运算都是内存级别的,所以性能比较高
2、数据结构简单,这些简单的数据结构的查找和操作的时间复杂度都是O(1)
3、多路复用和非阻塞IO,redis使用IO多路复用功能来监听多个socket连接的客户端,这样就可以用一个线程来处理多个情况,从而减少线程切换带来的开销,同时也避免了IO阻塞操作
4、避免上下文切换,省去了线程切换带来的时间和性能上的开销,而且单线程不会导致死锁的发生
2、IO多路复用是什么?
redis使用非阻塞IO,这样读写流程不再是阻塞的,读写方法都是瞬间完成并返回的,也就是它会采用能读多少读多少,能写多少写多少的策略来执行IO操作。这样会存在一个问题就是当我们在执行读操作时有可能只读了一部分数据,当缓冲区满了我们的数据还没写完,那么生效的数据何时写就是一个问题
IO多路复用就是解决上述问题,使用IO多路复用最简单的方法是使用select函数,该函数是操作系统给用户程序的API接口,==用于监控多个文件描述符的可读和可写情况的,这样就可以监控到文件描述符的读写事件了。==当监控到相应的时间之后就可以通知线程处理相应的业务了,这样就保证了redis读写功能的正常执行。
不过现在操作系统基本上不用select函数了,改为调用epoll函数(Linux)了,因为select函数在文件描述符非常多的时候性能非常差。
3、redis6.0 中的多线程?
redis单线程缺点导致redis的QPS很难得到有效提高(虽然已经够快了)。
redis在4.0版本虽然引入了多线程,但该版本的多线程只能用于大数据量的异步删除,对于非删除操作的意义不大
如果我们使用redis多线程就可以分摊redis同步读写IO的压力,以及充分利用多核cpu资源,虽然在redis中使用了IO多路复用,并且是基于非阻塞的IO进行操作的,但是IO操作本身就是阻塞的。
因此在redis6.0 中新增了多线程的功能来提高IO的读写性能,它的主要实现思路是将主线程的IO读写任务拆分给一组独立的线程去执行,这样就可以使用多个socket的读写并行化了,但redis的命令依旧是主线程串行执行的
**注意:**redis6.0 是默认禁用多线程的,但可以通过配置文件redis.conf 中的io-threads-do-reads 等于 true 来开启。除此之外我们还需要设置线程的数量才能正确的开启多线程的功能,修改redis的配置,比如设置 io-threads 4,表示开启4个线程。
设置线程数一定要小于机器的CPU核数
127.0.0.1:6379> set key1 v1 #设置值
127.0.0.1:6379> get key1 #获取值
127.0.0.1:6379> keys * #获得所有的值
127.0.0.1:6379> EXISTS key1 #判断某一个key是否存在
127.0.0.1:6379> APPEND key1 hello #追加字符串,如果当前key不存在,就相当于setkey
127.0.0.1:6379> STRLEN key1 #获取字符串的长度
---------------------------------------------------------------------------------
#i++
#步长 i+=
127.0.0.1:6379> set views 0 #初始化浏览量为0
127.0.0.1:6379> incr views
127.0.0.1:6379> incr views #自增1 浏览量变为1
127.0.0.1:6379> decr views #自减1 浏览量减一
127.0.0.1:6379> INCRBY views 10 #可以设置步长,指定增量!
127.0.0.1:6379> INCRBY views 5
127.0.0.1:6379> DECRBY views 6
(integer) 9
---------------------------------------------------------------------------------
#字符串范围 range
127.0.0.1:6379> set key1 hello,world
OK
127.0.0.1:6379> GETRANGE key1 0 4 #截取字符串[0,3]
"hello"
127.0.0.1:6379> GETRANGE key1 0 -1 #获取全部字符串
"hello,world"
#替换
127.0.0.1:6379> set key1 abcdef
OK
127.0.0.1:6379> SETRANGE key1 2 x #替换指定位置开始的字符串
(integer) 6
127.0.0.1:6379> get key1
"abxdef"
---------------------------------------------------------------------------------
# setex (set with expire) 设置过期时间
# setnx (set if no exist) 不存在再设置 (在分布式锁中会常常使用)
127.0.0.1:6379> setex key1 30 hello #设置key3的值为hello,30秒后过期
OK
127.0.0.1:6379> ttl key1
(integer) 23
127.0.0.1:6379> setnx mykey redis #如果mykey 不存在,创建mykey
(integer) 1
127.0.0.1:6379> keys *
1) "mykey"
127.0.0.1:6379> ttl key1
(integer) -2
127.0.0.1:6379> setnx mykey MongoDB #如果mykey存在,创建失败
(integer) 0
127.0.0.1:6379> get mykey
"redis"
---------------------------------------------------------------------------------
mset
mget
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3 ##同时获取多个值
1) "v1"
2) "v2"
3) "v3"
# msetnx 用于所有给定 key 都不存在时,同时设置一个或多个 key-value 对。
127.0.0.1:6379> msetnx k1 v1 k4 v4 # msetnx 是一个原子性操作,要么一起成功,要么一起失败!
(integer) 0
127.0.0.1:6379> get k4
(nil)
########################
getset #先get然后再set
#如果不存在值,则返回nil
#如果存在值,获取原来的值,并设置新的值
String类似的使用场景:value除了是我们的字符串还可以是数字!
基本数据类型,列表
在redis里面,可以把列表当成栈、队列、阻塞队列
所有的 list 命令都是以 l 开头的
127.0.0.1:6379> LPUSH list one #将一个值或多个值,插入到列表头部(左
127.0.0.1:6379> LRANGE list 0 -1 #获取list中的值
127.0.0.1:6379> LRANGE list 0 1 #通过区间获取具体的值
127.0.0.1:6379> RPUSH list four #将一个值或多个值,插入到列表尾部(右)
---------------------------------------------------------------------------------
127.0.0.1:6379> LPOP list #移出list的第一个元素
127.0.0.1:6379> RPOP list #移出列表的最后一个元素
---------------------------------------------------------------------------------
127.0.0.1:6379> LINDEX list 0 #通过下标获得 list 中的某一个值
---------------------------------------------------------------------------------
Llen
LLEN list #返回列表的长度
---------------------------------------------------------------------------------
移除指定的值!
Lrem list 1 one #移出list 集合中指定个数的value,精确匹配
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "two"
3) "one"
127.0.0.1:6379> lrem list 2 two
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "one"
---------------------------------------------------------------------------------
trim 截断
ltrim list 1 2 #通过下标截取指定的长度,这个list已经被改变了,只剩下截取的值了
---------------------------------------------------------------------------------
rpoplpush #移出列表的最后一个元素,将它移动到新的列表中
---------------------------------------------------------------------------------
lset 将列表中指定下标的值替换未另外一个值,更新操作
注意:修改列表中指定下标的值,要求列表和下标必须存在
---------------------------------------------------------------------------------
linsert #将某个具体的value插入到列表中某个元素的前面或者后面
Set中的值是不能重复的
---------------------------------------------------------------------------------
sadd #set集合中添加元素
smembers #查看指定set的所有值
sismember #判断某一个元素是否在set集合中
scard #获取set集合中的元素个数
srem #移出set集合中的指定元素
srandmember #随机抽选出一个元素,也可以随机抽选出指定个数的元素(后面带指定个数)
spop #随机删除一些set集合中的元素
smove (set1)(set2)(值) #将一个指定的值,移动到另外一个set中
sdiff #两个set集合的差集
sinter #两个set集合的交集(比如:共同好友就可以这样实现)
sunion #两个set集合的并集
Map集合,key-map,此时值是一个map集合。本质上和String类型没有太大区别,还是一个简单的 key-value
127.0.0.1:6379> hset myhash filed1 yang #set一个具体 key-value
127.0.0.1:6379> hget myhash filed1 #获取一个字段值
127.0.0.1:6379> hmset myhash f1 hello f2 world #set多个 key-value
127.0.0.1:6379> hmget myhash f1 f2 #获取多个字段值
127.0.0.1:6379> HGETALL myhash #获取全部的数据
---------------------------------------------------------------------------------
127.0.0.1:6379> hdel myhash f1 #删除hash指定key字段!对应的value值也就消失了
hlen #获取hash表的字段数量
HEXISTS myhash f2 #判断hash中的指定字段是否存在
HKEYS myhash #只获得所有的字段
HVALS myhash #只获得所有的值
HINCRBY #指定增量
hsetnx #如果不存在则可以设置,如果存在则不能设置
应用场景:
有序集合底层数据结构是跳跃链表
在set的基础上,增加了一个值
127.0.0.1:6379> zadd myset 1 one #添加一个值
zadd myset 2 two 3 three #添加多个值
ZRANGE myset 0 -1 #获取zset中的所有值
ZRANGEBYSCORE salary -inf +inf #显示全部的用户,从小到大
ZRANGEBYSCORE salary -inf +inf withscores #显示全部的用户并且附带成绩
ZRANGEBYSCORE salary -inf 3000 #3000一下的全部用户
ZREVRANGEBYSCORE salary +inf -inf #显示全部的用户,从大到小
zrem #移除有序集合中的指定元素
zcard #获取有序集合中元素的个数
---------------------------------------------------------------------------------
zcount #获取指定区间的成员数量
应用场景:
GEO底层的实现原理其实就是Zset!我们可以使用Zset命令来操作GEO
用于去重计算,可以接受误差
位运算,可以统计用户活跃数,打卡数之类
Redis 事务本质:一组命令的集合。一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行
一次性、顺序性、排他性!执行一序列的命令
Redis 事务没有隔离级别的概念
所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行
Redis 单条命令是保存原子性的,但是事务不保证原子性
redis 的事务
正常执行事务
127.0.0.1:6379> multi #开启事务
OK
#命令入队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
1) OK
2) OK
3) "v2"
4) OK
放弃事务
DISCARD 取消事务。事务队列中的命令都不会被执行
编译型异常(代码有问题!命令有错)事务中所有的命令都不会被执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k3 #错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec #执行事务的时候报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k2 #所有的命令都没有执行
(nil)
运行时异常(1/0),如果事务队列中不存在语法型错误,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr k1 #执行的时候会失败
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range #虽然第一条命令报错了。但是依旧正常执行成功了
2) OK
3) OK
4) "v3"
监控 Watch
悲观锁:
乐观锁:
Redis 测监视测试
watch相当于内置版本号了,每次修改都会有记录
正常执行成功
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money #监视 money 对象
OK
127.0.0.1:6379> multi #事务正常结束,数据期间没有发生变动,这个时候就正常执行成功
OK
127.0.0.1:6379(TX)> DECRBY money 20
QUEUED
127.0.0.1:6379(TX)> INCRBY out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20
测试多线程修改值,使用 watch 可以当作redis的乐观锁操作
127.0.0.1:6379> watch money #监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> DECRBY money 10
QUEUED
127.0.0.1:6379(TX)> INCRBY out 10
QUEUED
127.0.0.1:6379(TX)> exec #执行之前,另外一个线程,修改了我们的值,这个时候,就会导致事务执行失败
(nil)
如果修改失败,获取最新的值就好
要使用 Java 来操作 Redis
什么是Jedis
是 Redis 官方推荐的 Java 连接开发工具,使用 Java 操作 Redis 中间件
测试
<!--导入jedis的依赖-->
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
</dependencies>
1、为数据设置超时时间
除了字符串自己有设置过期时间方法外,其他方法都需要依赖expire方法来设置时间,如果没有设置时间,那缓存就是永不过期。如果设置了时间,之后又不让缓存永不过期,使用persist key
2、采用LRU算法动态将不用的数据删除
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了把内存数据持久化到硬盘文件,以及通过备份文件来恢复数据的功能,即Redis持久化机制。
RDB持久化是把当前Redis中全部数据生成快照保存在硬盘上。RDB持久化可以手动触发,也可以自动触发。
手动触发
save 和 bgsave 命令都可以手动触发RDB持久化
save命令
save 命令会阻塞 Redis服务,直到RDB持久化完成。当Redis服务存储大量数据时,会造成较长时间的阻塞,不建议使用。
bgsave命令
和 save 命令不同的事:Redis 服务一般不会阻塞。Redis进程会执行 fork 操作创建子进程,真正的持久化是在子进程中执行的(调用rdbSave),主进程会继续提供服务。
bgsave命令具体流程:
自动触发
自动触发的 RDB 持久化都是采用 bgsave 的方式,减少 redis 进程的阻塞。自动触达场景:
RDB的优缺点:
优点:rdb文件是一个紧凑的二进制压缩文件,是redis在某个时间点的全部数据快照,所以使用rdb恢复数据的速度远远比AOF快,适合备份、全量复制、灾难恢复等
缺点:每次进行bgsave 操作都要执行fork操作创建子进程,重量级操作,频繁执行成本过高,所以无法做到实时持久化。(如果服务器意外宕机或者断电,无法做到把全部数据都备份成功)
AOF持久化是把每次写命令追加写入日志中,当需要恢复数据时重新执行AOF文件中的命令就可以了。AOF解决了数据持久化的实时性。
AOF持久化默认是关闭的,修改redis.conf配置文件并重启,即可开启AOF持久化功能。
AOF本质是为了持久化,持久化对象是Redis内每一个key的状态,持久化的目的是为了在Reids发生故障重启后能够恢复至重启前或故障前的状态。相比于RDB,AOF采取的策略是按照执行顺序持久化每一条能够引起Redis中对象状态变更的命令,命令是有序的、有选择的。
AOF持久化流程:
文件同步策略
文件触发重写操作
手动触发
直接调用bgrewriteaof命令,如果当时无子进程执行会立刻执行,否则安排在子进程结束后执行。
自动触发
自动触发由Redis的周期性方法 serverCron 检查在满足一定条件时触发
两个参数:
当同时满足这两个条件时,AOF文件重写就会触发。
Redis 发布订阅是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道
图示:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
Redis 发布订阅命令
这些命令被广泛用于构件即时通信应用,比如网络聊天室、实时推送等
测试:
订阅端:
127.0.0.1:6379> SUBSCRIBE qq #订阅一个频道 qq
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "qq"
3) (integer) 1
#等待推送的消息
1) "message" #消息
2) "qq" #哪个频道的消息
3) "hello,qq" #消息的内容
1) "message"
2) "qq"
3) "welcome to qq"
发送端:
127.0.0.1:6379> PUBLISH qq "hello,qq" #发送者发布消息到频道
(integer) 1
127.0.0.1:6379> PUBLISH qq "welcome to qq" #发送者发布消息到频道
(integer) 1
127.0.0.1:6379>
主从复制:把一台Redis服务器的数据复制到其他Redis服务器上,前者称为主节点Master,后者称为从节点Slave,数据的复制是单向的,只能从Master单向复制到Slave,一般Master以写操作为主,Slave以读操作为主,实现读写分离。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点,但一个从节点只能有一个主节点。
主从复制的作用:
**全量复制:**从机每次连接主机时会全量复制,把主机的全部数据复制到主机
**增量复制:**从机连上主机后,对于主机后面更新的数据,会只针对这部分数据同步更新给主机。
**核心功能:**在主从复制的基础上,哨兵引入了主节点的自动故障转移。
哨兵Sentinel会作为一个独立的进程独立运行,通过发送命令,等到Redis服务器响应,从而监控运行的多个Redis服务器。
原理:哨兵是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的Master并将所有Slave连接到新的Master。所以整个运行哨兵的集群的数量不得少于3个节点
哨兵模式的作用:
故障转移机制:
注意:客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观操作和故障转移操作。
主节点的选举:
什么是Redis集群
Redis3.0加入了Redis集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的master节点上面,从而解决海量数据的存储问题。对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一Redis实例一样。
Redis也内置了高可用机制,支持N个master节点,每个master节点都可以挂载多个slave节点,当master节点挂掉时,集群会提升它的某个slave节点作为新的master节点
为什么需要Redis集群
单实例的Redis缓存足以应对大多数的使用场景,也能实现主从故障迁移。但是在某些场景下,单实例Redis缓存会存在的几个问题:
1、写并发
Redis单实例读写分离可以解决读操作的负载均衡,但对于写操作,仍然是全部落在了master节点上面,在海量数据高并发场景,一个节点写数据容易出现瓶颈,造成master节点的压力上升。
2、海量数据的存储压力
单实例Redis本质上只有一台Master作为存储,如果面对海量数据的存储,一台Redis的服务器就应付不过来了,而且数据量太大意味着持久化成本高,严重时可能会阻塞服务器,造成服务请求成功率下降,降低服务的稳定性。
Redis集群解决了存储能力受到单机限制,写操作无法负载均衡的问题。
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求到要到数据库去查询,造成缓存穿透。
解决方案:
1、缓存空对象:如果一个查询返回的数据为空,我们仍然把这个空结果进行缓存,但他的过期时间会很短,一般不超过5分钟
缓存空对象带来的问题:
2、布隆过滤器拦截:在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一段时间段失效(大批key失效/热点数据失效),大量请求直接到达存储层,存储层压力过大导致系统雪崩
解决方案:
1、加锁排队:在缓存失效后,通过加锁或者队列来控制数据库写缓存的线程数量。⽐如对某个 key 只允许⼀个线程查询数据和写缓存,其他线程等待;
2、数据预热:可以通过缓存reload机制,预先去更新缓存,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存实现的时间点尽量均匀;
3、可以把缓存层设计成高可用的,即使个别节点。个别机器宕机,依然可以提供服务。
4、采用多级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其它级别缓存兜底、
缓存击穿是指当前key是一个热点key(例如一个秒杀活动),并发量非常大,大并发集中对这一个点进行访问,当这个点(缓存)失效的瞬间,这些请求发现缓存过期一般都会从后端数据库加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端数据库压垮。
解决方案:
1、设置热点数据永不过期
从缓存层面看,没有设置过期时间,所以不会出现热点key过期后产生的问题
2、加互斥锁
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁。
四种同步策略
1、先更新缓存,再更新数据库
2、先更新数据库,再更新缓存
3、先删除缓存,再更新数据库
4、先更新数据库,再删除缓存
更新缓存:
优点:每次数据变化都及时更新缓存,所以查询时不容易出现未命中的情况
缺点:更新缓存的消耗比较大。如果数据需要经过复杂的计算再写入缓存,那么频繁的更新缓存,就会影响服务器的性能。如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取给数据
删除缓存:
优点:操作简单,无论更新操作是否复杂,都是将缓存中的数据直接删除
缺点:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库。(一般情况下,删除缓存是更优的方案)
先删除缓存再更新数据库:
先更新数据库再删除缓存
第一种和第二种方案没有人使用,因为第一种方案存在的问题是:如果更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。
第二种方案存在的问题是:并发更新数据库场景下,会将脏数据刷到缓存
目前常用的是第三种和第四种方案。
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3vtuwevgbfms4