服务端为什么要叫缓存?
如果在不加缓存,则客户端的请求则直接打在了db 数据库上,当有一天服务端接收大量查询,则可能导致数据返回慢或者宕机;
对于频繁读取操作, 缓存在缓存中,以减少对数据库的访问压力,从而提高服务端的访问数;
一般请求下的请求会如下:
客户端请求到server端,server 则会判断去查缓存是否存在, 如果不存在,则直接查询db,并将db查询结构返回客户端
如果存在, 则直接从缓存中查询, 并将缓存的数据返回客户端;
从代码片段
public Object getGoodsInfo(String goodsId) {
//查询缓存是否存在数据
Object cacheObj = getCache(goodsId);
if (cacheObj == null) {
//缓存中不存在数据,则访问数据库
Object dbObject = selectDbById(goodsId);
setCache(goodsId, dbObject);
return selectDbById(goodsId);
}
return cacheObj;
}
但是这样会造成问题:
缓存穿透,缓存击穿,缓存雪崩
缓存穿透
1.当一个不存在的id的查询,则会先如果缓存同时不存在则会直接访问db,但db查询也不存在, 多次这样的查询, 每次都要去数据库在查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库, 这样就缓存穿透了;
a、**接口校验。**在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。
b、缓存特定标识。当访问缓存和DB都没有查询到值时,可以将特定标识(如;200, 0 等)写进缓存,但是设置较短的过期时间,该时间需要业务特性来设置。
c、布隆过滤器。 布隆过滤器的原理可以自行百度,我也只知道他是跟多次hash,根据概率来返回结果。
缓存击穿
2.当一个key值,在某一时刻发生高并发量的访问(如key值时间过期,或者突然成为热点数据), 访问缓存不存在,则直接访问db, 造成瞬时数据库请求量大,可能会造成卡顿或者宕机; 造成缓存击穿;
a、热数据,设置永远不过期。
b、互斥锁加锁对请求进行同步。锁内逻辑:再次查询缓存,查不到转查数据库并且进行数据缓存,后面的请求就直接缓存,也避免再次查数据库。
缓存雪崩
由于大量缓存失效或者缓存整体不能提供服务,导致大量的请求到达db,会使db负载增加(大量的请求;
a.过期时间,每一个 key 选择合适的过期时间,避免大量的 key 在同一时刻同时失效
b.数据预热,对于即将来临的大量请求, 知道那些会成为热key,将数据提前缓存在Redis中,并设置不同的过期时间。
c. 多级缓存, 本地缓存+ redis+ 其他缓存等;
d. 加锁的方式: 互斥锁重建缓存(如redis 的lua 分布式锁等),避免大量请求访问db;
redis lua 分布式锁
伪代码:
public Object getGoodsInfoLock(String goodsId) {
//查询缓存是否存在数据
Object cacheObj = getCache(goodsId);
if (cacheObj != null) {
return cacheObj;
}
try {
/** 缓存中不存在数据,则访问数据库
* 拿到数据锁, 在将数据从缓存中读取,
* 使用lua脚本
* 这里解释一下为什么是不使用 SETNX + EXPIRE 方式
* 因为:
* setnx和expire两个命令分开了,「不是原子操作」。如果执行完setnx加锁,
* 正要执行expire设置过期时间时,进程crash或者要重启维护了。那么这个锁就永久,不会失效。
* 设置成功,返回 1 。 设置失败,返回 0
*/
if (tryLock(goodsId).equals(1)) {
Object dbObjet = selectDbById(goodsId);
setCache(goodsId, dbObjet);
unLock(goodsId);
return dbObjet;
}
}catch (Exception exception) {
System.out.println("其他异常:" + exception.getMessage());
}
return null;
}
KEYS[1] 为key ,ARGV[1] 为value , ARGV[2] 为 时间;
如果有多个参数, 也可用 hash 使用
redis_lock.lua
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
redis_unlock.lua
if redis.call("exists",KEYS[1]) == 0 then
return 1
end
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
那么如果锁超时了,且业务代码并没有执行成功, 这会被其他重新设置?
续锁。 在启动个守护线程,如每2秒去 继续key 的有效期。 如果机器宕机了, 那就等过期时间失效。
如果定时器发现 key 还存在,则判断延长过期时间
redis_expire_key.lua
if redis.call("exists",KEYS[1]) == 1 then
return redis.call('expire',KEYS[1],ARGV[2])
end
return 0
end
缓存与数据库一致性问题
当数据库的数据发生修改时, 我们一般会时先修改数据库,还是缓存呢?比如数据库表中已经存在 id=10 ,name=张三的值;
当数据发生修改时,是先同步db, 后删除缓存?或者是先删除缓存在同步到db?
a.先同步db, 后删除缓存
2 个接口:
修改接口: 先修改db ,后再同步缓存到数据库
查询接口: 判断缓存是否存在, 不存在则直接查询缓存;
在并发情况下 , 是的数据库的数据和缓存不一致;
只是理论上会发生,概率很小,这问题的基础在于 缓存失效读+并发更新db;
b 先删除缓存,在更新db
同样的也会存在缓存和数据库不一直问题;
对于不一致的问题;
所有的写要都要以数据库为准,并所有的缓存都应该设置过期时间;
解决:
延迟双删策略(对于单个数据库来说, 他保证不了绝对的成功;)
1 先删除缓存,
2 在更新数据库
3 等待一段时间,在删除 (至于时间多久,则需要根据自己的的实际需求)
4 再次删除缓存
缺点:
1)延迟时间难以确认
到底是延迟一秒或者是几秒,这个其实很难确认,所以这个时间很难确定。
2)无法做到绝对的成功
即使延迟时间确认了,也根据上面的图,也会发现, 做不了100% 的成功;
3)读写分离或者一主多从
对于这样的数据机构,db binglog 的日志同步本身就是要花时间去同步,然而这个同步时间 没法去考量
如果db实现读写分离,或者读取的从db服务器为及时同步到 数据呢? 在从db查询不到数据的情况下,读取的值也是旧值,也有可能导致缓存和数据不一致的问题 。
那么就在于这个使用第二次删除的策略了,既要保证从db 能同步成功完成后再去删除缓存, 也要保缓存成功删除;
这个这样的问题,是不知道什么时候主从成功同步? 如果知道了,那么我们通知节点删除;那么是否可以监听一下binlog 日志呢?
binlog 日志
binlog即binary log,二进制日志文件,也叫作变更日志(update log)。它记录了数据库所有执行的DDL和DML等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、show等) 。它以事件形式记录并保存在二进制文件中。通过这些信息,我们可以再现数据更新操作的全过程。 mysql 主从之间的数据同步依靠异步的读取binlog 的方式;
最佳策列:
删除缓存+更新数据库+ cancal (监听binlog日志,删除缓存)
什么是Canal? 以及他能做什么?
Canal主要用途是基于MySQL数据库增量日志解析,提供增量数据订阅和消费。简单认为Canal就是一个简单的增量数据同步工具,能帮我们监听到数据的变化;
原理如下;
●canal模拟MySQL slave的交互协议,伪装自己为MySQL slave,向MySQL master发送dump协议。
●MySQL master收到dump请求,开始推送binary log给slave(即canal )。
●canal解析binary log对象(原始为byte流),再推送到MySQL、kafka、ElasticSearch等存储应用当中。
这种方案不适合频繁的修改。以及对一致性要求高的场景;当然这种方案还是有缺点的; 不过缺点相比其他还是可以接受的