概述
目的:
- 1.降低请求的处理时间,进而提高服务的吞吐能力(并发量)
- 2.保护数据库
原理:内存的访问速度 远远 高于 硬盘的访问速度
场景:读多写少
技术选型:
- redis:单线程,五种数据模型。主流
- memcache:多线程,kv结构
实现:
- 1.注解:@EnableCache @Cacheable
- 2.代码:jedis spring-data-redis(默认的序列化器RedisTemplate)
步骤:
- 1.先查缓存,如果缓存中命中,则直接返回
- 2.如果缓存没有命中,则查询数据库或者远程调用 查询数据 放入缓存
RedisTemplate的序列化器
XML | JSON | String | JDK |
---|---|---|---|
性能最低 | 稍低 | 高 | 最高 |
内存占用最高 | 稍低 | 最低 | 较高 |
可读性较差 | 最高 | 较高 | 没有 |
最常用的是:
StringRedisTemplate:对象 集合 需要手动序列化
缓存写的一致性问题:
读取缓存数据一致性一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。
不管先保存到MySQL,还是先保存到Redis都面临着一个保存成功而另外一个保存失败的情况。
不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:
-
如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
-
如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。
解决:
1.基于mysql的binlog日志(canal)
2.消息队列
1.双写模式(写数据库,写缓存)
1.先写redis:写redis成功了(新) --> mysql失败了(旧) --> 数据不一致
2.先写mysql:写mysql成功了(未提交) --> 写redis成功了(新) --> 代码异常或者服务器宕机了,mysql会回滚(旧) --> 数据不一致
2.失效模式(缓存失效(删除缓存),写数据库——删除redis 高并发下出现数据不一致)
1.先删redis:
a用户:先删了redis(空) --> 再去写mysql --> 提交(mysql 新)
b用户: 查询数据 --> 先查redis --> 再查myql,放入redis(旧)
2.先写mysql
a用户:写mysql成功了 (未提交)--> 删除redis(空) --> 提交数据(新)
b用户: 查询数据 --> redis --> mysql,放入redis(旧)
3.双删模式
1.删除缓存(常规操作——防止旧数据堆积)
2.写mysql
3.提交事务
4.异步删除(借助于AOP后置通知)(保险操作——防止中间操作改变数据)
4.canal中间件-阿里开源
基于mysql的binlog日志
canal的工作原理就是把自己伪装成MySQL slave,
模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,
MySQL mater收到canal发送过来的dump请求,
开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,
比如MySQL,Kafka,Elastic Search等等。
缓存常见问题:并发读
1.缓存穿透:大量请求访问不存在的数据,由于数据不存在,对应的缓存可能就没有,请求就会直达数据库,导致mysql服务器宕机。
解决方案:数据即使为null也缓存 布隆过滤器
2.缓存雪崩:由于缓存时间相同,导致大量缓存数据同时过期,此时请求就会直达数据库
解决方案:给缓存时间添加随机值
3.缓存击穿:一个热点的key过期,此时大量请求直达数据库
解决方案:加分布式锁
分布式锁
jdk没有提供分布式锁的实现方案,只能自己实现或者使用第三方框架实现
1.基于关系型数据库实现
2.基于redis实现
3.基于zk实现
可靠性:zk > redis == mysql
性能:redis > zk > mysql
实现复杂度:zk > mysql > redis
基于redis实现。锁:
1.手写分布式锁
**特征**:
1.独占排他:setnx
2.防止死锁:
1.客户端程序(index)获取redis锁之后,服务器立马宕机,导致死锁。
给锁添加过期时间:expire set key value ex 30 nx
2.不可重入
3.保证锁操作的原子性
加锁和设置过期时间之间:set key value ex 30 nx
判断和解锁之间:lua
4.防误删:解铃还须系铃人
删除之前要先判断是否自己的锁才能解锁
5.可重入:Hash + lua脚本
6.自动续期:
定时任务:Timer
lua脚本:
7.集群情况下锁机制可能失效:RedLock算法(红锁算法,redis特有的)
1.获取客户端当前时间
2.序列化的从N个相互独立的redis节点获取锁,获取锁的方式采用之前的方式,每个节点获取锁的过期时间一般不超过50
3.计算逃逸时间=系统当前时间-step的时间
4.计算剩余锁定时间=总的锁定时间 - step3逃逸时间
5.如果获取锁失败,把锁定成功的节点解锁
操作:
1.加锁:独占排他 防死锁(过期时间) 可重入(hash + lua) 自动续期
1.初步:setnx 保证独占排他 问题:没有过期时间可能会导致死锁 不可重入
2.改进:set key value ex 30 nx 解决死锁(添加过期时间) 加锁和过期时间之间可以保证原子性 问题:不可重入
3.可重入:hash + lua脚本 实现可重入 问题:自动续期
4.续期:Timer定时器 + lua脚本 实现自动续期
2.解锁:防误删(判断) 判断和删除原子性 可重入解锁
1.初步:del指令 简单释放锁 问题:可能会导致误删
2.先判断再删:lua脚本 防止误删同时保证了原子性 不可重入
3.可重入:hash + lua脚本
4.续期:取消定时器
3.重试:递归
redis对lua脚本提供了主动支持,redis中输出的是lua脚本的返回值,而不是输出
由于redis是单线程,它接受及处理请求,遵守one-by-one规则
eval script numKeys key arg
script:脚本
numkeys:lua脚本所需KEYS元素数量
全局变量:a=5 redis中的lua脚本不支持全局变量
局部变量:local a=5
分支控制:
if 条件
then
。。
elseif 条件
then
。。
else
。。
end
防误删脚本:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
if redis.call('get', KEYS[1]) == ARGV[1]
then
return redis.call('del', KEYS[1])
else
return 0
end
KEYS:lock
ARGV:uuid
2.redisson框架-分布式锁
1.引入依赖
2.java配置 初始化一个RedissonClient
3.可重入非公平锁
RLock lock = redissonClient.getLock("xx");
lock.lock()/unlock()
公平锁
RLock lock = redissonClient.getFairLock("xx");
lock.lock()/unlock()
读写锁
RReadWriteLock rwLock = redissonClient.getReadWriteLock("xx");
rwLock.readLock().lock()/unlock()
rwLock.writeLock().lock()/unlock()
信号量
RSemaphore semaphore = redissonClient.getSemaphore("xx");
semaphore.trySetCount(资源量)
Semaphore.tryAcquire()
semaphore.tryRelease()
闭锁(倒计数器)
RCountDownLatch cdl = redissonClient.getCountDownLatch("xx");
cdl.trySetCount(6)
cdl.await()/countDown()
使用AOP实现缓存封装
1.自定义缓存注解GmallCache
2.通过AOP给注解赋能
布隆过滤器
BloomFilter,判断一个元素是否存在。以牺牲精确度换取空间和时间效率的算法
结构:
1.二进制数组
2.一系列的hash函数
特征:
1.判定一个数据存在,可能不存在
2.判定一个数据不存在,就一定不存在
3.删除困难:CountingBloomFilter
准确度因子:
1.hash函数个数:个数越多精确度越高,但性能就会越高
2.二进制数组的长度:长度越长精确度越高,但是占用空间越大
场景:
1.缓存防止缓存穿透
2.垃圾邮件
3.骚扰电话
4.爬虫程序中防止重复爬取
实现:
1.google的guava工具库 langs
2.redisson
3.redis插件 违背了devops