Redis缓存之实战篇——基于黑马点评

目录

1.短信登录

1.1基于Session实现登录

1.2 基于Redis实现登录

1.3登录拦截器的优化

2.查询缓存

2.1缓存的定义和添加缓存

2.2 数据缓存更新策略

2.2.1主动更新

Cache Aside

Read/Write Through

Write Back

2.2.2数据缓存更新策略总结 (高一致性需求)

2.3 查询缓存相关问题

2.3.1缓存穿透

2.3.2缓存雪崩

2.3.3缓存击穿(热点 Key 问题)

3.优惠秒杀

3.1 基本实现秒杀券的下单功能

3.2 超卖现象

3.3分布式锁 

3.3.1常见的分布式锁 

3.3.2 Redis分布式锁的基本实现

3.3.3 添加线程标识解决分布式锁的误删问题

3.3.4 Lua脚本解决分布式锁的原子性问题

3.3.5 Redission分布式锁

3.3.5.1 Hash结构解决锁的可重入问题

3.3.5.2 发布订阅结合信号量解决锁重试问题

3.3.5.3 watchDog解决锁超时释放问题

3.3.5.4 主从一致性问题

3.4 Redis秒杀优化

3.4.1 基于Redis实现秒杀减少库存

3.4.2 基于Redis实现一人一单限制

3.4.3 基于JVM阻塞队列的异步下单

3.4.4 Redis消息队列实现异步秒杀

3.4.4.1 基于list实现的消息队列

3.4.4.2 基于PubSub实现的消息队列

3.4.4.3 基于Stream实现的消息队列

3.4.4.4三者对比


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 也是企业级开发中比较好的一个实现分布式锁的方案。

MySQLRedisZookeeper
互斥利用 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的阻塞队列完成异步下单,存在以下问题:

  1. jvm的内存限制问题

  2. 数据安全问题: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实现的消息队列

 

3.4.4.4三者对比

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值