目录
1.短信登录
1.1基于Session实现登录
存在问题:服务器的内存中记录客户端信息,存在数据丢失问题。且多台Tomcat不共享session存储空间,当请求切换到不同的tomcat服务时导致数据丢失的问题。
1.2 基于Redis实现登录
Redis作为数据库存储数据,不同的tomcat服务器共享数据。把数据存入Redis,集群的Redis可以替代session。
登录流程
用户提交手机号,服务端生成验证码。以key-value的结构将key:手机号和value:验证码存入Redis数据库。value选择String类型就可以。
用户收到验证码,再次向服务器提交手机号和验证码。服务器校验手机号和验证码,随机生成token作为key存入Redis数据库中,value为用户信息。选择Hash结构存储用户信息,因为每个字段独立,比较好去DRUD,内存占用少。
服务器发送token给客户端,每次请求客户端都会携带着token。服务器拦截器校验token后放行。
1.3登录拦截器的优化
用户请求进去拦截器,服务器试着去获取请求头内的token,根据token去查询用户信息,判断是否拦截,保存在ThreadLocal,刷新token的有效期
但是,这个拦截器是拦截需要登录之后才需要进行请求的路径,那我如果一直在访问的是不需要拦截的页面的话,我还是会过期?这就不合理。所以我们需要在这个拦截器前面再加个拦截器,然后在新增拦截器上进行保存ThreadLocal和刷新有效期,对拦截器进行功能拆分。
使用Redis的好处:
1.存在内存中。2.存储结构灵活。3.适用于分布式存储结构,共享数据。4.可以设置key的有效期expire。
2.查询缓存
2.1缓存的定义和添加缓存
定义
数据交换的缓冲区。cache临时存储数据,读写性能较高。
缓存的作用:降低后端负载;提高服务读写响应速度。
添加缓存 (读操作)
客户端的请求首先到达Redis数据库,Redis查询数据,数据如果存在(命中)直接返回给客户端。数据不存在(未命中),则查询数据库,将查询到的结果写入Redis缓存中,Redis返回信息给客户端。
2.2 数据缓存更新策略
数据库和Redis之间可能存在数据不一致性问题。
当数据库发生改变,要解决这个问题Redis要及时更新。 (写操作)
三种策略:
·内存淘汰:Redis自带的内存淘汰机制
·过期淘汰:利用expire命令给数据设置过期时间
·主动更新:主动完成数据库与缓存的同时更新
2.2.1主动更新
Cache Aside
缓存调用者在更新数据库的同时完成对缓存的更新
优缺点:
一致性良好;实现难度一般
先操作数据库再删除缓存。
删除缓存还是更新缓存?
更新缓存会产生无效更新,并且存在较大的线程安全问题;删除缓存本质是延迟更新,没有无效更新,线程安全问题相对较低。
先操作数据库还是缓存?
先更新数据库再删除缓存,在满足原子性的情况下,安全问题概率较低。先删除缓存,再更新数据库,安全问题概率较高。
由于数据库的操作速度比操作缓存的速度慢,所以操作缓存的时候极低概率会被操作数据库的线程抢去CPU,反过来就会出现线程安全问题,所以采用先更新数据库再删除缓存。
Read/Write Through
缓存与数据库集成为一个服务,服务保证两者的一致性,对外暴露API接口。调用者调用API,无需知道自己操作的是数据库还是缓存,不关心一致性。
优缺点:
一致性优秀;实现复杂;性能一般
Write Back
缓存调用者的CRUD都针对缓存完成。由独立线程异步的将缓存数据写到数据库,实现最终一致。
优缺点:
一致性差;性能好;实现复杂
2.2.2数据缓存更新策略总结 (高一致性需求)
读操作:
客户端的请求首先到达Redis数据库,Redis查询数据,数据如果存在(命中)直接返回给客户端。数据不存在(未命中),则查询数据库,将查询到的结果写入Redis缓存中,并设置超时时间。(超时剔除的更新缓存策略)Redis返回信息给客户端。
写操作:
先写数据库在删除缓存。
保证事务的原子性。单体系统采用事务管理;分布式系统采用分布式事务机制。
2.3 查询缓存相关问题
2.3.1缓存穿透
问题描述:
客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
解决方案:
- 缓存空对象:对于不存在的数据也在Redis建立缓存,值为空,并设置一个较短的TTL时间。
- 布隆过滤:利用布隆过滤算法,在请求进入Redis之前先判断是否存在,如果不存在则直接拒绝请求。
2.3.2缓存雪崩
问题描述:
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
2.3.3缓存击穿(热点 Key 问题)
问题描述:
一个被 高并发访问 并且 缓存中业务较复杂的 Key 突然失效,大量的请求在极短的时间内一起请求这个 Key 并且都未命中,无数的请求访问在瞬间打到数据库上,给数据库带来巨大的冲击。
解决方案:
- 互斥锁:查询缓存未命中,获取互斥锁,获取到互斥锁的才能查询数据库重建缓存,将数据写入缓存中后,释放锁。
- 逻辑过期:查询缓存,发现逻辑时间已经过期,获取互斥锁,开启新线程;在新线程中查询数据库重建缓存,将数据写入缓存中后,释放锁;在释放锁之前,查询该数据时,都会将过期的数据返回。
热点key缓存永不过期,而是设置一个逻辑过期时间,查询到数据时通过对逻辑过期时间判断,来决定是否需要重建缓存。重建缓存也通过互斥锁保证单线程执行。重建缓存利用独立线程异步执行。
获取锁使用Redis中的setnx方法。
如果 Redis 中没有这个 Key,则插入成功;如果有这个 Key,则插入失败。通过插入成功或失败来表示是否有线程插入 Key,插入成功的 Key 则认为是获取到锁的线程;释放锁就是将这个 Key 删除,因为删除 Key 以后其他线程才能再执行
setnx
方法。
/**
* 获取互斥锁
*/
private boolean tryLock(String key) {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", TTL_TEN, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放互斥锁
*/
private void unLock(String key) {
redisTemplate.delete(key);
}
3.优惠秒杀
3.1 基本实现秒杀券的下单功能
在分布式系统中,订单ID要全局唯一。且满足 唯一性,高可用,高性能,递增性,安全性。
下单时要判断:秒杀是否开始,库存是否充足。
3.2 超卖现象
多线程下存在超卖现象。常见解决方案是加锁。
实现乐观锁的常见方式:
· 版本号法 数据库表多维护一列 version。判断读数据库时和更新数据库时的version是否一致。一致则操作成功,否则失败。
·用数据本身有没有变化进行判断
3.3分布式锁
多线程集群下使用分布式锁。分布式锁需要满足多进程可见,互斥,高可用,高性能,安全性。
3.3.1常见的分布式锁
MySQL:MySQL 本身带有锁机制,但是由于 MySQL 性能一般,所以采用分布式锁的情况下,使用 MySQL 作为分布式锁比较少见。
Redis:Redis 作为分布式锁比较常见,利用 setnx 方法,如果 Key 插入成功,则表示获取到锁,插入失败则表示无法获取到锁。
Zookeeper:Zookeeper 也是企业级开发中比较好的一个实现分布式锁的方案。
MySQL | Redis | Zookeeper | |
---|---|---|---|
互斥 | 利用 MySQL 本身的互斥锁机制 | 利用 setnx 互斥命令 | 利用节点的唯一性和有序性 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 好 | 一般 |
安全性 | 断开链接,自动释放锁 | 利用锁超时时间,到期释放 | 临时节点,断开链接自动释放 |
3.3.2 Redis分布式锁的基本实现
为了保证添加和删除锁操作的事务原子性。将set操作和expire写到同一个语句中。
3.3.3 添加线程标识解决分布式锁的误删问题
解决方案:
3.3.4 Lua脚本解决分布式锁的原子性问题
判断锁标识和释放锁是两个操作,有原子性问题。使用Lua脚本解决。
Lua将redis命令写入一个脚本中,执行脚本。
3.3.5 Redission分布式锁
3.3.5.1 Hash结构解决锁的可重入问题
可重入锁的概念就是:自己可以获取自己的内部锁。
假如有一个线程 T 获得了对象 A 的锁,那么该线程 T 如果在未释放前再次请求该对象的锁时,如果没有可重入锁的机制,是不会获取到锁的,这样的话就会出现死锁的情况。
Redis记录key和ThreaId外,还需要记录重入次数,需要用hash结构存储。每次的Theadid一样时,次数+1;直到次数为0的时候才可以删除锁。
3.3.5.2 发布订阅结合信号量解决锁重试问题
锁重试:获取锁失败后重新获取
tryLock(waitTime,leaseTime,TimeUnit)
waitTime:获取锁的等待时长,获取锁失败后等待waitTime再去获取锁
leaseTime: 锁自动失效时间,这里测试锁重试不需要用到
3.3.5.3 watchDog解决锁超时释放问题
3.3.5.4 主从一致性问题
3.4 Redis秒杀优化
3.4.1 基于Redis实现秒杀减少库存
新增优惠券的同时加入到Redis缓存中
3.4.2 基于Redis实现一人一单限制
编写lua脚本,基于lua完成一人一单
-- 1.参数列表
-- 1.1 优惠券id
local voucherId = ARGV[1]
-- 1.2用户id
local userId = ARGV[2]
-- 2.数据key
-- 2.1 库存key
local stockKey = 'seckill:stock:'..voucherId
-- 2.2 订单key
local orderKey = 'seckill:order:'..voucherId
-- 3.脚本业务
-- 3.1判断库存是否充足
if (tonumber(redis.call('get', stockKey)) <= 0)
then return 1
end
-- 3.2判断用户是否下单 sismember orderKey userId
if(redis.call('sismember',orderKey,userId)==1)
then return 2
end
-- 3.3扣库存 incrby stockKey -1
redis.call('incrby',stockKey,-1)
-- 3.4下单 sadd orderKey userId
redis.call('sadd',orderKey,userId)
return 0
3.4.3 基于JVM阻塞队列的异步下单
使用JVM的阻塞队列完成异步下单,存在以下问题:
-
jvm的内存限制问题
-
数据安全问题:jvm的内存数据没有持久化,每当服务器重启或者宕机或者从阻塞队列取的时候遇到异常,数据都会丢失
解决方法: 消息队列模型
消息队列
生产者
消费者
3.4.4 Redis消息队列实现异步秒杀
Redis提供了三种不同方式来实现消息队列
1.list结构:模拟消息队列
2.Pubsub:基本的点对点模型
3.Stream :比较完善的消息队列模型
3.4.4.1 基于list实现的消息队列
3.4.4.2 基于PubSub实现的消息队列
3.4.4.3 基于Stream实现的消息队列