第一篇 基础知识
随着大数据时代到来,数据急速增长,导致关系型数据库SQL越来越不够用。高性能、可扩展的数据库变得越来越重要。此时,非关系型数据库NoSQL应运而生。这里的NoSQL是 Not Only SQL的简称。
第一章 进入MongoDB和Redis的世界
非关系型数据库的佼佼者——文档型数据库MongoDB和键值数据库Redis。
1.1 非关系型数据库的产生背景和分类
关系型数据库遇到的瓶颈
2008年左右,网站、论坛、社交网络开始高速发展,关系型数据库的地位受到了很大的挑战:
- 难以应付每秒上万次的高并发数据写入
- 查询上亿量级的速度极其缓慢
- 分库、分表形成的字库达到一定规模后难以进一步扩展
- 分库、分表的规则可能会因为需求变更而发生变更
- 修改表结构困难
场景:对数据连表的查询需求不是那么强烈,也并不需要在数据写入后立刻读取,但对数据的读取和并发写入速度有非常高的要求
非关系型数据库的分类及特点
-
键值数据库——Redis、Flare
这类数据库用于极高的读写性能,适合用于处理大量的高访问负载;
-
文档型数据库——MongoDB、CouchDB
这类数据库满足了海量数据的存储和访问需求,同时对字段要求不严格,可以随意地增加、删除、修改字段,且不需要预先定义表结构,所以适用于各种网络应用;
-
列存储数据库——Cassandra、Hbase
这类数据库查找速度快,可扩展性强,适合用作分布式文件存储系统;
-
图数据库——InfoGrid、Neo4J
这类数据库利用“图结构”的相关算法,适合用于构建社交网络和推荐系统的关系图谱;
图数据库的理论基础是图论,通过节点、边和属性对数据进行表示和存储。
1.2 MongoDB和Redis可以做什么
- MongoDB适合存储大量关联性不强的数据;
MongoDB中的数据以"库"——“集合”——“文档”——"字段"结构进行存储,不需要预先定义表结构,数据的字段可以任意变动,并发写入速度也远远超过传统关系数据库
- Redis有多种数据结构,适合多种不同的应用场景:
缓存:Redis的字符串、哈希表两种数据结构适合用来存储大量的键值对信息,实现高速缓存;
队列:使用“列表”数据结构,可以实现普通级和优先级队列的功能;使用“有序集合”数据结构,可以实现优先级队列;使用“哈希表”数据结构,可以实现延时队列。
去重: 利用“集合”数据结构,可以实现小批量数据的去重;利用“字符串”数据结构的位操作,可以实现布隆过滤器,从而实现超大规模的数据去重;利用Redis自带的HyperLogLog数据结构,可以实现超大规模数据的去重和计数。
排行榜/积分板:有序集合”功能可以实现积分板功能,还能实现自动排序、排名功能。
实现“发布/订阅”功能
第二章 数据存储方式的演进
第二篇 快速入门
第三章 MongoDB入门
3.1 MongoDB基本操作
创建数据库:
use testdb
插入一条数据:insertOne()
db.getCollection('user').insertOne({
_id:NumberLong(2),
name:"liyuan"
})
批量插入数据:insertMany()
db.getCollection('user').insertMany([
{
_id:NumberLong(3),
name:"liyuan1",
age:20
},
{
_id:NumberLong(4),
name:"liyuan2"
}])
注意:无论是插入一条数据还是插入多条数据,每一条数据被插入 MongoDB后都会被自动添加一个字段“_id”。“_id”读作“Object Id”,它是由时间、机器码、进程pid和自增计数器构成的。
八卦一下:_id的前8位字符转换为十进制就是时间戳。例如“5b2f2e24e0f42944105c81d2”,前8位字符“5b2f2e24”转换为十进制就是时间戳“1529818660”,对应的北京时间是“2018-06-2413:37:40”
查询所有数据:find({})
db.getCollection('user').find({})
查询固定值数据:find({‘字段1’:‘值1’,‘字段2’:‘值2’})
db.getCollection('user').find({
name:"liyuan1"
})
查询范围值数据:find({‘字段1’:{‘操作符1’:‘边界1’,‘操作符2’:‘边界2’}})
db.getCollection('user').find({
name:"liyuan1",
age:{
'$gte':10,
'$lt':20
}
})
限定返回某些字段:find({查询条件过滤},{限定字段})
“find”命令可以接收两个参数:第1个参数用于过滤不同的记录,第2个参数用于修改返回的字段。如果省略第2个参数,则MongoDB会返回所有的字段。
其中,用于限定字段的字典的Key为各个字段名。其值只有两个——0或1。 如果值为0,则表示在全部字段中剔除值为0的这些字段并返回。如果值为1,则表示只返回值为1的这些字段。不能同时出现0和1
db.getCollection('user').find({
name:"liyuan"
},{
age:1,
name:1
})
查询满足条件的数据的条数:count({过滤条件}) 或者 find({过滤条件}).count()
db.getCollection('user').count({
name:'liyuan'
})
限定返回数据的条数:limit()
db.getCollection('user').find({
name:"liyuan"
}).limit(1)
跳过若干条数据:skip()
db.getCollection('user').find({
name:"liyuan"
}).skip(2)
对查询结果进行排序:sort({‘字段1’:-1或1})
其中,字段的值为-1表示倒序,为1表示正序。
db.getCollection('user').find({
name:"liyuan"
}).sort({
age:-1
})
更新单条数据:updateOne({查询过滤条件},{‘$set’:{‘字段1’:‘新值1’,‘字段2’:‘字段2’}})
updateOne第一个参数和find的第一个参数一样,第2个参数是一个字典,它的Key为“$set”,它的值为另一个字典。这个字典里面是需要被修改的字段名和新的值
db.getCollection('user').updateOne({
name:'liyuan'
},{
$set:{
age:20
}
})
扩展:自增自减式更新,关键字为 $inc
db.getCollection('user').updateOne({
name:'李白'
},{
$inc:{age:7}
}
)
批量更新:updateMany({查询过滤条件},{‘$set’:{‘字段1’:‘新值1’,‘字段2’:‘字段2’}})
上面两个更新命令参数完全一致,只是一个是只更新第一条满足条件的数据,第二个是更新所有满足条件的数据
删除单条数据:deleteOne({查询过滤条件})
db.getCollection('user').deleteOne({
name:'liyuan'
})
批量删除多条数据:deleteMany({查询过滤条件})
数据去重:distinct(’字段名’, 查询过滤条件)
distinct()可以接收两个参数:
第1个参数为字段名,表示对哪一个字段进行去重。
第2个参数就是查询命令“find()”的第1个参数。distinct命令的第2个参数可以省略。
db.getCollection('user').distinct('name',{})
在MongoDB中返回的数据是一个数组,里面是去重以后的这个字段的值,能否去重以后再带上其他字段呢?答案是,用“distinct()”命令不能实现。要实现这个功能,需要学习第7章的内容。
3.2在java中使用MongoDB
第四章 暂无
第五章 Redis入门
5.1五种基础数据结构
String
List
Set
Hash
Sort set
基础略过,这边讲几个细节:
String类型数据的set方法
set name liyuan
set name liyuan NX|XX
set不携带NX|XX参数时,无条件添加或覆盖key对应的value
set携带NX参数表示key不存在则添加value,如果存在key,直接放弃操作;
set携带XX参数表示key存在则覆盖value,如果不存在key,直接放弃操作。
注意:这边的set key value NX 可以做分布式锁。
SCAN命令
字符串只应用在小量级的数据记录中。如果数据量超过百万级别,那么使用字符串来保存简单的映射关系将会浪费大量内存。此时需要使用Redis的另一种数据结构——Hash。储存相同量级的数据,Hash 结构消耗的内存只有字符串结构的1/4,但查询速度却不会比字符串差。
如果Redis中有大量Key,尤其是key数量达到百万千万级别的时候,那么执行“keys *”命令会对Redis性能造成短暂影响,甚至导致Redis失去响应。因此,绝对不应该在不清楚当前有多少Key的情况下冒然列出当前所有的Key。【redis官网上提示在一个1百万key的数据库中执行一次keys * 查询耗时40毫秒】
取而代之的是使用SCAN,每次只会返回少量元素,不会造成阻塞服务器的问题;SCAN是一个基于游标的迭代器, 这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程 ; 当SCAN命令的游标参数(即cursor)被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束。
SCAN cursor [MATCH pattern] [COUNT count]
SCAN 0 match *k* count 5
注意,这边的count 5并不代表输出符合条件的5个key,而是限定服务器单次遍历的字典槽位数量。针对每种数据结构,都有单独的遍历迭代器,分别为zscan / hscan / sscan
HSCAN key cursor [MATCH pattern] [COUNT count]
Set一些命令
smembers key:获取集合中所有数据,如何集合中数据量极大,应该慎用,可能会导致系统的I/O资源瞬间被耗尽;
spop key count:随机弹出集合中count条数据,count默认为1;
sinter key1 key2 key3 …:求多个集合的交集
sunion key1 key2 key3 …:求多个集合的并集
sdiff key1 key2 key3 …:求多个集合的差集
Hash表
在Redis中,使用哈希表可以保存大量数据,且无论有多少数据,查询时间始终保持不变。Redis的一个哈希表里面可以储存232─1(约等于43亿)个键值对。
使用哈希表不仅可以减少Redis的个数,还能优化储存空间。Redis官方就特别说明,哈希表对存储结构进行过特殊的优化,储存相同的内容,占用的内存比字符串要小很多。
使用字符串保存一百万个键值对需要21GB的存储空间,而改为哈希表以后,只需要5GB的存储空间。
有序集合——天生就自带排序功能
有序集合里面的数据跟集合一样,也是不能重复的,但是每一个元素又关联了一个分数(Score),根据这个分数可以对元素进行排序。分数可以重复。
传统数据库方式实现排行榜缺点:
- 排行榜会实时更新,数据每一次变化都要排序,对数据库性能造成影响
- 频繁更新数据,导致数据库性能下降。
- 数据量太大时排序时间缓慢。
- 对被排序字段添加索引会占用更多空间。
利用Redis的有序集合进行排序:
- 添加数据
zadd key score1 value1 score2 value ... scoren valuen
- 修改分数——如果值不存在,则自动创建,并把修改的分数作为初始评分。
zincrby key score1 value
- 查询某个值的排名
zrank key value
- 查询某个值的分数
zscore key value
- 基于分数范围排序
zrangebyscore/zrevrangebyscore key minscore maxscore WITHSCORES LIMIT 切片开始位置 结果数量
其中,WITHSCORES和LIMIT都是关键字。
WITHSCORES可以省略。省略以后,只有值没有评分。
如果不需要对结果进行切片,则“LIMIT 切片开始位置 结果数量”也可以省略。
- 基于位置范围排序
zrange/zrevrange key
第三篇 高级应用
第七章 Mongo高级语法
多条件查询:
如果第一个参数的查询过滤条件存在AND或OR的多条件查询时,有两种方式实现:隐式(只有AND有隐式)、显式(使用$and关键字)
TODO
嵌入式文档应用:一个原则,使用"."来一层层表示内层字段
db.getCollection("user").find({
'skin.additionType':"法术伤害",
'skin.additionValue':{$gte:10,$lte:20}
})
数组应用:
数组包含(2种方式)或者不包含($ne)某些数据:
db.getCollection("user").find({
'skills':{
"name" : "将进酒",
"desc" : "消耗技能",
"damageValue" : 80.0
}
})
db.getCollection("user").find({
'skills':{
$eq:{
"name" : "将进酒",
"desc" : "消耗技能",
"damageValue" : 80.0
}
}
})
根据数组长度查询数据:【提示:$size只能查询具体某一个长度的数组,不能查询长度大于或小于某个值的数组】
db.getCollection("user").find({
'skills':{
$size:3
}
})
根据数组下标索引进行查询:
db.getCollection("user").find({
'skills.1.damageValue':{
$gte:80,
$lte:120
}
})
聚合查询:
MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。有点类似sql语句中的 count(*)
MongoDB中聚合的方法使用自带的aggregate()方法
一个复杂的聚合由若干个阶段完成,前一个阶段的输出是后一个阶段的输入,通过接力的方式完成从原始数据到最终数据的转换。
基本语法:
collection.aggregate{[阶段1,阶段2,...,阶段n]}
说明:聚合可以由0或n个阶段完成,每一个阶段都是一个字典【键值对象】,不同的阶段负责不同的事情,每一个阶段有一个关键字。有专门负责筛选数据的阶段“ m a t c h ”,有专门负责字段相关的阶段“ match”,有专门负责字段相关的阶段“ match”,有专门负责字段相关的阶段“project”,有专门负责数据分组的阶段“$group”等。聚合操作有几十个不同的阶段关键字。
-
筛选数据——一般并非所有数据都需要参与处理,所以第一步一般是筛选
关键字:$match
语法:
collection.aggregate({'$match':{和find第一个参数一样的表达式}})
db.getCollection('user').aggregate([ { '$match':{ age:{$gte:20,$lte:30} } } ]) <==> db.getCollection('user').find({ age:{$gte:20,$lte:30} })
-
筛选并返回部分字段——有时候并不需要返回所有字段
关键字:$project
语法:
collection.aggregate({'$project':{和find第二个参数一样的表达式}})
db.getCollection('user').aggregate([ { $project:{name:1,age:1} } ])
其实这边我们使用了 m a t c h 和 match和 match和project才实现了find的功能。
- 添加新字段——凭空添加新字段并赋值(可以是常量值或是字段值)
db.getCollection('user').aggregate([ { $project:{name:1,age:1,slogan:'hello,everyone',age_1:'$age'} } ])
- 修改现有字段的数据——将原本返回的数据直接赋予新值
db.getCollection('user').aggregate([ { $project:{name:'common name',age:1} } ])
- 抽取嵌套字段——对于嵌入式文档数据,可以将内层的字段抽取到外层,变成普通字段,
db.getCollection('user').aggregate([ { $project:{ _id:0, skinName:'$skin.name', additionType:'$skin.additionType', additionValue:'$skin.additionValue' } } ])
-
处理字段特殊值——如果对于$project,就像将1赋给某个字段,或我的常量就带有 $
关键字:$literal
db.getCollection('user').aggregate([
{
$project:{
_id:0,
slogan:{$literal:'$name'},
age:{$literal:1}
}
}
])
-
分组操作
关键字:$group
作用:根据给出的字段Key,把所有Key的值相同的记录放在一起进行运算;这些运算包括常见的“求和( s u m )”“计算平均数( sum)”“计算平均数( sum)”“计算平均数(avg)”“最大值( m a x )”“最小值( max)”“最小值( max)”“最小值(min)”等。
- 去重:distinct(),得到一个去重之后的该字段的数组
db.user.distinct('name')
- $group自动去重
db.user.aggregate([{ '$group':{'_id':'$sex'} }])
分组操作和distinct函数都能实现去重操作,但是返回数据格式不一样,distinct返回数组,分组操作返回的是纪录列表。
- 分组并计算统计值
db.user.aggregate([{ '$group':{ '_id':'$被去重的字段名', 'min_age':{$min:'$字段名'}, 'max_age':{$max:'$字段名'}, 'average':{$avg:'$字段名'}, 'sum':{$sum:'$字段名'} } }])
db.user.aggregate([{ '$group':{ '_id':'$sex', 'min_age':{$min:'$age'}, 'max_age':{$max:'$age'}, 'average':{$avg:'$age'}, 'sum':{$sum:'$age'} } }])
注意:这里用到了四个统计关键字
$max:求分组中某字段的最大值
$min:求分组中某字段的最小值
s u m :求分组中某字段的和,原则字段必须是数值型,如果是非数字字段,则返回 0 ; sum:求分组中某字段的和,原则字段必须是数值型,如果是非数字字段,则返回0; sum:求分组中某字段的和,原则字段必须是数值型,如果是非数字字段,则返回0;sum的值如果用数字1,则统计每个分组内的记录数
$avg:求分组中某字段平均值,原则上必须是数值型,如果是非数字字段,则返回null
- 去重并选择最新或最老的数据——默认是插入的顺序
db.user.aggregate([{ '$group':{ '_id':'$sex', 'age':{'$last':'$age'}, 'name':{'$first':'$name'} } }])
-
拆分数组
关键字:$unwind
作用:它的作用是把一条包含数组的记录拆分为很多条记录,每条记录拥有数组中的一个元素
语法:
collection.aggregate({'$unwind':'$字段名'})
db.user.aggregate([ { '$unwind':'$skills' } ])
注意:一次只能拆开一个数组,如果想展开多个数组,则可以让第一次运行的结果再走一次“$unwind”阶段
db.user.aggregate([ { '$unwind':'$skills' }, { '$unwind':'$parents' } ])
-
联集合查询——所谓的联集合查询,相当于SQL中的联表查询
关键字:$lookup
语法:
mainCollection.aggregate([ {'$lookup':{ 'from':'被查集合名', 'localField':'主集合字段', 'foreignField':'被查集合字段', 'as':'保存查询结果的字段名' } } ])
db.getCollection('hero').aggregate([ {'$lookup':{ 'from':'hero_type', 'localField':'type', 'foreignField':'_id', 'as':'hero_info' } } ]) db.getCollection('hero_type').aggregate([ {$lookup:{ 'from':'hero', 'localField':'_id', 'foreignField':'type', 'as':'heros' } } ])
注意:这里连表查出来的hero_info和heros都是一个数组,在数组中是一个嵌入式的文档。
-
美化查询结果——使用 p r o j e c t 和 project和 project和unwind关键字
u n w i n d 可以展开数组, unwind可以展开数组, unwind可以展开数组,project可以提取嵌入式文档数据到外层结构中
db.getCollection('hero_type').aggregate([ {$lookup:{ 'from':'hero', 'localField':'_id', 'foreignField':'type', 'as':'heros' } }, {'$unwind':'$heros'}, {$project:{ '_id':'$heros._id', 'heroSex':'$heros.sex', 'heroName':'$heros.name', 'heroType':'$name', 'typeDesc':'$desc' } } ])
小结:MongoDB的聚合操作可以把各个不同的阶段组合起来,上一个阶段的输出作为下一个阶段的输入,从而实现非常灵活而强大的功能。
-
聚合操作阶段的组合方式
$match筛选——如果对原集合数据进行筛选,可以放任何位置,如果是对联集合查询结果进行筛选,应该放后面。
db.getCollection('hero_type').aggregate([ {$lookup:{ 'from':'hero', 'localField':'_id', 'foreignField':'type', 'as':'heros' } }, {'$unwind':'$heros'}, {$project:{ '_id':'$heros._id', 'heroSex':'$heros.sex', 'heroName':'$heros.name', 'heroType':'$name', 'typeDesc':'$desc' } }, {$match:{ 'heroType':'法师' } } ])
从性能上考虑,建议把“$match”放在最前面,这样可以充分利用到 MongoDB 的索引,提高查询效率。
-
第八章 MongoDB额优化和安全建议
8.1提高MongoDB的读写性能
批量插入与逐条插入:
插入与更新:
使用索引提高查询速度:
索引是一种特殊的数据结构,它使用了能够快速遍历的形式记录了集合中数据的位置。如果不使用索引,则每一次查询数据 MongoDB 都会遍历整个集合;而如果使用了索引,则MongoDB会直接根据索引快速找到需要的内容。
-
创建索引
语法:
collection.createIndex(keys,options)
语法中 Key 值为你要创建的索引字段,1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可。
db.getCollection('hero').createIndex({ 'age':'1' })
options选项为可选参数,可选参数有:
-
background:true/false,默认为false;如果为False,那在创建索引时,这个集合就不能被查询也不能被写入,但是速度快,如果设置为True,那么创建索引的速度会慢一些,但是不影响其他程序读写这个集合。
-
unique:true/false,默认false
-
name:索引名称 ;如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。
-
引入Redis,以降低MongoDB的读取频率
增添适当冗余信息,以提高查询速度
8.2 提高MongoDB的安全性
第九章 Redis高级数据结构
9.1 发布消息/订阅频道
Redis的“发布/订阅”模式是一种消息通信模式,实现了一对多的消息实时发布功能。
发布信息:
命令:publish 频道名 信息
订阅频道:
命令:subscribe 频道名1 频道名2 频道名n
9.2 Redis高级数据结构
BloomFilter:
HyperLogLog:
BitMap:
GeoHash:
补充
SQL VS NOSQL
SQL:指关系型数据库,主要代表:SQL Server,Oracle,MySQL;通常是基于表结构存储,需要预先定义
关系型数据库适合存储结构化数据,如用户的帐号、地址
NOSQL:指非关系型数据库,主要代表:MongoDB,Redis;存储结构更加灵活和可扩展,可以是JSON文档、哈希表或者其他方式
NoSQL适合存储非结构化数据,如文章、评论等海量的、结构多样不确定数据
MySQL、MongoDB和Redis
Mysql:关系型数据库,磁盘数据库,无论数据还是索引都存放在硬盘中,到要使用的时候才交换到内存中,能够处理远超过内存总量的数据。
MySQL是持久化存储,存放在磁盘里面,检索的话,会涉及到一定的 IO
MongoDB:非关系型数据库,内存数据库,但是数据实质存储在磁盘上,并且占用空间要比MySQL大很多,是典型的的空间换时间。不支持事务操作。
MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。其优势在于查询功能比较强大,擅长查询JSON数据,能存储海量数据
Redis:非关系型数据库,内存数据库,所有数据都是放在内存中的,持久化是使用 RDB 方式或者 aof 方式。
数据库选用
缓存穿透、击穿、雪崩
雪崩
定义: 某一时刻发生大规模的缓存失效的情况
产生原因:缓存服务器宕机;热点数据集中失效
解决: 采用集群,降低服务宕机的概率 ; 使用 Hystrix进行限流 & 降级 ; 设置不同的失效时间
穿透
定义: 查询不存在数据 ——不断请求缓存和数据库中都没有的数据 , 导致数据库压力过大,严重会击垮数据库
产生原因:使用非法的id恶意攻击;
分析: 之所以会发生穿透,就是因为缓存中没有存储这些空数据的key,从而导致每次查询都到数据库去了;
解决: 接口层增加校验;缓存空值并设置过期时间;BloomFilter( 利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return);
针对于一些恶意攻击,攻击带过来的大量key 是不存在的,那么我们采用第一种方案就会缓存大量不存在key的数据。此时我们采用第一种方案就不合适了,我们完全可以先对使用第二种方案进行过滤掉这些key。针对这种key异常多、请求重复率比较低的数据,我们就没有必要进行缓存,使用第二种方案直接过滤掉。而对于空数据的key有限的,重复率比较高的,我们则可以采用第一种方式进行缓存。
击穿
定义: 在高并发的情况下,大量的请求同时查询同一个key时,此时这个key正好失效了,就会导致同一时间,这些请求都会去查询数据库
这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。
产生原因:
分析: 击穿是因为多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。 其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
解决: 设置热点数据永远不过期(比如首页数据) ; 分布式互斥锁
小结:缓存雪崩、穿透和击穿,是缓存最大的问题,要么不出现,一旦出现就是致命性的问题
缓存和数据库的正确的打开方式——数据库和缓存的一致性
引言: 通常来说,在我们的系统中会把数据永久保存在DB中,并且冗余一份数据在缓存中。读请求优先从缓存读取数据,没有再从DB读取 。减少DB压力,提高请求响应速度。
引入缓存后,读操作会先去缓存中看下,如果没有命中缓存,才去读取数据库,然后把读取出来的数据再放到缓存中去,这样下一次读操作就可以命中缓存了,如果命中缓存,就可以直接把数据返回出去了。
写操作,除了修改数据库,还需要删除缓存,因为不删除缓存,读的操作读到的永远都是缓存中的旧数据。
先删除缓存,后修改数据库
问题: 两个并发的读写操作
- 一个写的操作先进来,把缓存删除了;
- 在写操作还没有更新数据库的时候,一个读的请求又进来了,发现没有命中缓存,就去数据库把老数据取出来了;
- 写操作更新了数据库;
- 读操作把老数据放在了缓存中。
先修改数据库,后删除缓存
问题: 两个并发的读写操作
- 读操作先进来,发现没有缓存,去数据库中读数据,这个时候因为某种原因卡了,没有及时把数据放入缓存;
- 写的操作进来了,修改了数据库,删除了缓存;
- 读操作恢复,把老数据写进了缓存。
这样就造成了数据库、缓存不一致,不过,这个概率出现的非常低,因为这需要在没有缓存的情况下,有读写的并发操作,在一般情况下,写数据库的操作要比读数据库操作慢得多,在这种情况下,还要保证读操作写缓存晚于写操作删除缓存才会出现这个问题,所以这个问题应该可以忽略不计。
还有一个场景:一个写的操作进来,修改了数据库,但是删除缓存的时候 ,由于Redis服务器出现问题了,或者网络出现问题了,导致删除缓存失败,这样数据库保存的是新数据,但是缓存里面的数据还是老数据,妥妥的数据库、缓存不一致啊。
延迟双删
先删除缓存,后修改数据库,最后延迟一定时间,再次删除缓存