短信登录
基于session的短信登录
- 发送验证码,session保存验证码。
- 验证验证码,session保存用户信息。
- 校验登录状态,判断session里是否保存用户信息。
使用ThreadLocal保存用户信息,是方便后面业务获取当前用户,避免传参和频繁从session中取对象。
基于Redis实现共享session登录
集群的session共享问题:多台tomcat不能共享session存储空间,当请求切换到不同tomcat服务器时导致数据丢失。
- 发送验证码,Redis保存验证码。
- 校验验证码,用户信息保存到Redis中,返回token给前端。
- 前端请求首部行中携带token(请求首部中添加Authorization字段)校验登录状态,判断Redis里是否有用户信息。
验证码:key是手机号,value是string类型,验证码。
用户信息:key是token,value是Hash类型,用户。
商户查询缓存
缓存,交换数据的缓冲区,存储数据的临时地方,读写性能好。
缓存的作用,降低后端数据库负载,提高读写效率降低响应时间。
添加Redis缓存
- 商铺请求先访问Redis,Redis命中直接返回数据。
- Redis未命中,查询数据库,如果数据库中存在数据,再写入Redis中,否则返回404。
缓存更新策略:
- 内存淘汰。内存淘汰自动删除部分数据。
- 超时剔除。给缓存增加TTL,到期自动删除缓存。
- 主动更新。先操作数据库,再删除缓存。
一致性也就是缓存中有值时,与数据库中一致。
低一致性需求:使用Redis自带的内存淘汰机制。
高一致性需求:使用主动更新策略,并以超时剔除为兜底。
主动更新:先删缓存再写数据库、先写数据库再删缓存、先写数据库再写缓存、读写穿透、写回。
读操作:
- 缓存命中直接返回。
- 缓存未命中则查询数据库,并写入缓存,设置TTL。
写操作:
- 先写数据库,再删除缓存。
- 确保数据库与缓存的操作原子性。
先删缓存再写数据库的问题?
更新操作删除缓存后还没更新数据库时,查询操作缓存未命中读数据库旧值,更新操作更新缓存,查询操作写入缓存旧值。
读写穿透:读写缓存数据库同步服务。
写回:只更新缓存,异步更新数据库。
缓存穿透
客户端请求的数据在数据库和缓存中都不存在,缓存永远无法生效,请求会打到数据库上给数据库带来压力。
常见的解决方案:缓存空对象和布隆过滤器。
缓存空对象:在查询时缓存和数据库都未没命中,在缓存中缓存空值设置TTL。在查询缓存的时候判断是否是空值(不是nil)。
布隆过滤器:所有可能请求的值存放在布隆过滤器中,请求来时先判断是否存在,不存在直接返回错误信息。
缓存雪崩
同一时间大量的缓存key同时失效或者Redis服务宕机,大量请求到达数据库带来压力。
解决方案:
- 设置随机失效时间。
- 缓存预热。
- Redis集群。
- 多级缓存。
- 业务添加多级缓存。
缓存击穿
热点key问题,被高并发访问并且缓存重建业务复杂的key突然失效,大量请求瞬间到达数据库带来压力。
解决方案:互斥锁和逻辑过期。
互斥锁方案
查询缓存未命中获取互斥锁,查询数据库重建缓存数据,写入缓存,释放锁。其他线程查询缓存未命中获取互斥锁失败,休眠重试直到缓存命中。
逻辑过期
缓存数据中添加逻辑TTL,查询缓存发现已过期,直接返回过期数据,获取互斥锁开启新线程重建缓存,写入缓存后重置逻辑TTL释放互斥锁。其他线程查询缓存发现已过期,获取互斥锁失败返回过期数据。
优惠券秒杀
全局ID生成器
分布式系统下生成全局唯一的ID工具。
基于Redis自增策略,订单ID由Redis中整型数据4个字节64位组成,1比特为0符号位,31位时间戳,32为序列号。使用Redis整型数据自增来生成ID。
秒杀券下单
实现优惠券秒杀:
- 判断秒杀是否开始或者结束
- 库存是否充足,扣减库存,添加订单
在高并发环境下,会出现超卖的问题(在库存量为1的时候,多个线程进入判断库存充足扣减订单,超卖订单)。
悲观锁:添加互斥锁,让线程串行执行,实现简单但性能一般。
乐观锁:不加锁,在更新时判断是否有其他线程在修改,性能好但成功率低。版本号法,在更新数据之前检查版本号,更新数据时修改版本号。
// 优化乐观锁成果率低的问题,在更新时同时检查订单是否大于0
seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
一人一单
在更新前判断数据库订单表中是否相同用户相同秒杀券的记录,由于没有加锁同样会出现超卖的问题。
由于不存在数据更新,所以无法使用乐观锁,使用悲观锁synchronized给创建订单上锁。
一人一单的并发安全问题:在集群模式下,同一个用户在不同服务器上的请求会获取不同的锁(不同的JVM,不同的锁监视器),会超单。
分布式锁
满足分布式系统或集群模式下多进程可见并且互斥的锁。
基于Redis分布式锁
版本1:基于Redis的setnx
命令实现分布式锁,设置TTL超时自动释放锁。
版本1的问题:线程1业务阻塞,锁TTL超时释放,线程2获取锁执行业务中,线程1执行完任务释放线程2的锁。
版本2:获取锁时存入线程标识(UUID+线程ID,不能只使用线程ID,不同JVM线程ID可能相同),释放锁时核对线程标识。
版本2的问题:线程1判断锁标识和释放锁之间阻塞,锁TTL超时释放,线程2获取锁执行业务前,线程1阻塞结束后释放锁。
版本3:使用Luna脚本完成判断线程标识和锁释放的原子性。
总结:基于Redis的分布式锁实现思路,利用setnx获取锁设置过期时间,保存线程标识。释放锁先判断线程标识是否一致,一致释放锁。
基于Redis分布式锁优化
基于setnx实现的分布式锁存在的问题:
- 不可重入。同一个线程无法多次获取同一把锁。
- 不可重试。获取锁尝试一次就返回。
- 超时释放。如果业务执行时间耗时长,导致锁提前释放。
- 主从一致性。Redis采用主从集群结构, 主节点宕机且锁数据未同步给从节点,导致锁失效。
Redisson分布式锁工具。
可重入锁的原理:利用Hash结构记录线程标识和重入次数。
可重试的原理:利用pubsub机制,订阅锁释放的信号尝试获取锁。
锁不过期的原理:开启watchdog定时更新TTL。
主从一致性的原理:联锁,每台Redis服务器独立,获取锁时需要半数以上的每台服务器都获取锁。
秒杀优化
业务流程:
- 查询优惠券,判断秒杀库存。读数据库。
- 查询订单, 判断一人一单。读数据库。
- 更新库存。写数据库。
- 创建订单。写数据库。
优化方式,同步下单变成异步下单,读写数据库分离。利用Redis完成库存余量、一人一单判断,完成抢单业务。再将下单业务放入阻塞队列,利用独立线程异步下单。
基于阻塞队列的异步秒杀问题:内存限制,阻塞队列使用JVM内存,高并发情况下大量订单占用JVM内存。数据安全问题,服务重启或者宕机,阻塞队列数据丢失。
基于Redis的消息队列实现异步秒杀
- 基于List双向链表实现。LPUSH和BRPOP阻塞获取。只支持单消费者,无法避免消息丢失。
- 基于PubSub实现。发布消息,订阅频道。不支持数据持久化,无法避免消息丢失。
- 基于Stream实现。多个消费者划分到一个消费者组,监听同一个队列。
Stream消费者组消息队列的特点:
- 消息分流。同一个消费者组的消费者之间竞争消息。
- 消息标识。消费者组维护一个标识,最后一个被处理的消息。
- 消息确认。消费者获取消息后,消息处于pending状态,在pending-list中,当消息处理返回ack确定后才会从list中移除。
秒杀优化券方案总结:利用lua脚本完成库存余量、一人一单判断,完成抢单业务。将下单信息放到消息队列,开启独立线程异步下单,扣减库存使用乐观锁。
达人探店
点赞
在Redis中记录以blogid为key,点赞用户为value的集合。
点赞排行榜:使用SortedSet,点赞时带时间为score。
好友关注
关注
user_id
和fans_id
多对多关系表中添加。
共同关注
为每个用户在redis添加一个关注集合set,求两个集合的交集。
关注推送
Feed流,无限下拉刷新获取新消息。
- 拉模式:用户从关注邮箱中拉去消息。读,延迟高。
- 推模式:发布消息到用户的邮箱中。写,延迟低。
- 推拉结合:粉丝多的采用拉,粉丝少的采用推。
基于推模式实现关注推送:blog保存到数据库的同时,推送到粉丝的邮件箱,收件箱使用Redis的SortedSet满足时间戳排序,查询收件箱数据时实现分页查询(滚动式分页,数据不断变化,数据的角标也在变化。维护上一次最小的时间戳和偏移量)。
附近商户
Redis GEO结构存储地理位置信息。
根据商铺类型分组,typeid作为key存入同一个GEO集合中,成员是店铺ID。