仿大众点评系统
使用redis登陆验证操作
后台生成一个随机的六位数验证码,存储到redis(手机号为key)。
用户得到验证码后,在客户端输入,校验,若成功,①保存用户到数据库,②存储用户信息为value、token为key到redis(如果session的话不能分布式,或者springsession来操作redis),③保存用户到ThreadLocal(每一个线程内部都有一个ThreadLocal,用处见下面拦截器),浏览器拿到token为cookie。
用一句话来说,在执行目标方法之前,判断用户的登录状态,并封装传递(用户信息)给controller。ThreadLocal是同一个线程的,作用域小,而且一个请求过来,拦截器,controller,service,dao都是同一个线程完成的,所以在这里面这个最合适。而session线程不一定安全???
下一次同一个浏览器进来时,先看基于cookie的authorization中拿用户token(若有token则会有用户,不过reis用户信息30min失效),redis有的话先从redis拿(注意,用了springsession的话,说法就是从session拿,但是springsession的store type也是用的redis),拿不到证明登陆失效,重新登录并且从数据库拿然后放到threadlocal和redis中。
注意:一开始 校验码 还有 用户token 都是存在session中的(每一个浏览器访问时会有cookie,根据它里面的sessionID就能得到session)。因为多个tomcat之间不共享session存储空间,所以存储到了redis中,数据共享,内存存储也快,keyvalue结构,String类型。
若是没有登录的时候,需要用拦截器拦截某些需要用户登录的请求网址,所以把登陆校验流程都放到了拦截器中,但是每一个controller需要用户信息,所以存到了ThreadLocal中。(每一个进入Tomcat的请求都是一个独立的线程,将来ThreadLocal就会在线程内开辟一个空间保存对应的用户,不同用户有独立的线程)
但是拦截的时候,由于设置了用户一段时间不操作任何就会登出(redis的用户token的时间),那么有些没有拦截到的就不会刷新token。所以此处优化了拦截器,放了两个拦截器,第一个拦截器拦截所有,redis有用户的话,刷新token,后面那个是需要拦截的请求网址,要是不存在,拦截到登录界面。
有关ThreadLocal
商铺缓存
策略:
低一致性:内存淘汰
高一致性,主动更新缓存,并用超时剔除兜底。先操作数据库,再删除缓存(这样更新的时候只是读到旧数据,而先删,可能更新后还是一直读取到旧数据)。但得保证数据库和缓存操作同时成功或者失败:单体,事务;分布式,TOC等分布式事务方案。
注:缓存穿透、缓存雪崩、缓存击穿,见redis
超卖问题
多个线程同时查询到有剩余,一起拿掉,就超卖了。
乐观锁CAS(成功率低,更新能用,插入用不了因为没有比较了),每次一发现库存没有被更改时时,才去设置。
但是悲观锁性能差。
一个用户只能下一单
单机:此处是插入,所以不能CAS因为没有比较,用悲观锁
集群下:需要使用分布式锁。因为集群下,全新的Tomcat,全新的JVM,每个JVM都有自己的锁监视器,分布式锁可以多个JVM用同一把锁。1 Mysql本身的互斥锁机制能回滚。2 redis的setnx,可以锁超时释放,高可用,高性能。3 zookeeper节点的唯一性和互斥性,临时节点。
方法一:基于redis的分布式锁(与谷粒商城讲的token机制差不多)
添加锁(setnx)和超时(兜底)需要原子性,所以两句话改成一句话。
需要加线程标识(uuid区分jvm+线程ID区分线程),释放锁之前判断是否是当前线程与锁中的线程是否一致,一致才释放。
判断和释放也要原子性(Lua脚本),不然的话,判断完成被阻塞(如GC的STW),否则只能等锁超时释放。
优点:满足互斥行,故障时由于超时机制依然能释放,提高线程安全。利用集群可以保证高可用和高并发。
缺点:不可重入,不可重试,超时释放(业务执行时间更长),主从一致性(主节点拿到锁,未同步就宕机)。
方法二:第三方Redission
Redisson是一个在Redis的基础上实现的Java驻内存数据网络。不仅提供了一系列的分布式java常用对象,还提供了许多分布式服务,包括分布式锁。
可重入锁
1 | boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); |
可重入锁原理:Lua保证原子性。获取锁不存在获取锁hash中的field(threadID)的value+1,已经存在则判断线程id标识是不是自己。释放锁,线程ID不是自己就返回,是自己的话,重入次数-1,重置有效期,若=0删除key。
Redisson分布式锁三个原理
- 可重入:利用hash类型结构记录线程ID和重入次数,(不用setnx的string类型的了,所以lua需要原子操作,判断key是否存在,不存在获取,存在判断是否自己重入)
- 可重试:利用信号量和PubSub实现等待唤醒获取锁失败的重试机制
- 超时续约:利用watchDog,每隔一段时间重置(在锁需要无限时间-1的情况下)。而且不传时间默认加的锁时间30s,所以如果异常出错了,锁也会删除。如果主动传,那么不会续约,所以一定要传的时间大于业务时间。
Redisson分布式锁还有一个原理,主从一致性
multilock,对每一个主节点都获取锁成功,才能执行任务
分布式锁总结
异步队列优化秒杀
先利用redis,判断库存余量(新增优惠券库存同时,直接保存到redis中)和校验一人一单(这两个放在lua中,原子操作),耗时短,需要速度快,完成抢单,所以放在redis中完成。
而查询优惠券,查询订单,减库存,创建订单4步都需要访问数据库,耗时长,利用独立线程异步下单。所以打算这两项分开。
抢购成功,将优惠券id和用户id封装到阻塞队列,然后后面开启线程任务,不断从阻塞队列获取信息,实现异步下单。
异步队列实现
基于List结构
LPUSH和BRPOP,或者RPUSH和BLPOP来实现。
优点:不受JVM内存限制。基于redis持久化,满足有序性
缺点:无法避免消息丢失(拿出后,未处理挂了),只支持单消费者
基于PubSub消息队列
优点:采用发布订阅模型,支持多生产、多消费
缺点:不支持数据持久化,无法避免消息丢失(发布的不会再redis保存),消息堆积有上限超出时数据丢失。
基于Stream的消息队列
Redis5.0新引入的一种数据类型,可以实现一个功能非常完善的消息队列。
==消费者组==:将多个消费者划分到一个组中,监听同一个队列。(多个客户端,可以同时读取不同的数据并行处理,加快了消费速度)。
优点:不受JVM内存限制。基于redis持久化,满足有序性
- 消息可回溯(永久保存)
- 多个消费者争抢消息,加快消费速度
- 可以阻塞读取
- 没有漏读的风险,会标记上一个读取的
- 消息确认,保证消息至少被消费一次
代码中使用Executors.newSingleThreadExecutor线程池去submit不断读取,若抛异常,处理pendinglist中没有得到ack的消息。
探店笔记
上传图片返回地址,然后还有一些标题、正文、图片存储地址等数据,
点赞功能
一个用户点击一次,set数据类型(无法排序,唯一)
所以,要加入根据用户点赞时间排序,可以使用SortedSet,以时间戳作为score
共同关注
关注的用户id,存储set数据类型,可以使用交集看共同好友。
关注推送
feed流。(==timeline==如朋友圈,智能排序如小红书推荐)
拉模式(读扩散):放到发件箱,用户在读的时候,才拉取
==推模式==(写扩散):要的时候,推送给每一个人,内存浪费
推拉结合:把粉丝分为大v(活跃粉丝 推速度快,普通粉丝 拉)和普通(粉丝少推)
一般用 推,kw以下级别,用推就好,因为拉实现复杂。新增探店笔记后,存储到数据库的同时,推送到粉丝的redis收件箱(根据时间戳排序),分页查询。
附近店铺
每日签到
Author: Jcwang
Permalink: http://example.com/2022/06/25/%E9%A1%B9%E7%9B%AEredis-practice/