Redis实战

image-20221202102058814

短信登录

基于Session实现登录

image-20221202103316067

image-20221204210746963

session共享问题

多台Tomcat不共享session存储空间,当请求切换到不同的Tomcat服务时导致数据丢失的问题

session的替代方案应该满足:

  • 数据共享
  • 内存存储
  • key-value结构

image-20221205120908299

基于Redis实现共享session登录

image-20221205122834128

image-20221205122911443

image-20221205123132178

image-20221205123306145

redis代替session考虑的问题

  • 选择合适的数据结构
  • 设置合适的key
  • 选择合适的存储粒度
  • 设置合适的有效期

解决状态登录刷新的问题

image-20221206100256416

值得注意,这里需要登录的路径才会走拦截器刷新token有效期,导致不需要登录即未拦截的路径没有刷新token有效期。

image-20221206100835173

商户查询缓存

什么是缓存

缓存就是数据交换的缓存区(称作Cache),是存贮数据的临时的地方,一般读写性能较高。

image-20221206104611652

image-20221206104759767

添加Redis缓存

image-20221206220709634

缓存更新策略

image-20221207105308463

三种常见的缓存读写策略

Cache Aside Pattern(旁路缓存模式)

在更新数据库的同时更新缓存

这是平时比较多的缓存读写模式,比较适合读多写少的场景

image-20221207105745436

image-20221207130436342

  • 同步时时,尽量选择删除缓存,在读少写多的场景下,避免对于缓存过多无效的写操作
  • 如何保证缓存和数据库数据的同步
    • 单体系统:将缓存和数据库操作放到一个事务里
    • 分布式事务,利用TTC等分布式事务方案
  • 先操作数据库,再删除缓存,原因如下图所示

image-20221207130033856

Read/Write Through Pattern(读写穿透)

将缓存和数据库的同步整合为一个服务,由服务来维护一致性,减轻应用程序的职责。调用者调用该服务,无需关心缓存的一致性的问题。开发比较少遇到的原因是性能问题以及服务开发和维护成本。

Write Behind Caching Pattern(异步缓存写入)

调用者操作缓存后,由其他线程异步的将缓存数据持久化到数据库,保证数据的一致性。缺点是数据一致性难以保证,存在数数据库还没有更新,缓存服务宕机的风险。

常用于一些数据经常变化,但是对数据一致性要求没有太高的场景,比如浏览量、点赞量等。

总结

image-20221207130139744

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,缓存不会生效,这些请求会到存储层去查询,大量请求就会落到数据库中。

攻击者利用不存在的key频繁攻击应用,大量请求攻击数据库,导致数据库压力过大甚至引起数据库服务的宕机。

解决方案

  • 接口校验

    在接口层增加权限校验,拦截一些不符合逻辑的请求

  • 缓存空对象

    缓存和数据库都没有时,将key-value写成key-空对象写入缓存,设置较短缓存有效期(减少数据一致性带来影响),有效降低攻击者短时间内反复用同一个id暴力攻击

    缺点:占用缓存中额外的内存开销,有可能造成短时间内数据不一致的问题

  • 布隆(bloomfilter类似于hash set结构)

    用于判断某个元素是否存在于集合中,不存在就直接返回。该过滤器的关键就是hash算法和容器的大小

    缺点:实现复杂,存在误判的可能性

image-20221207151744982

缓存空对象

image-20221207153521213

总结

image-20221207165554772

缓存雪崩

缓存雪崩指数据大量缓存的key同时失效或者redis宕机,导致大量请求到达数据库。

解决方案

  • 缓存数据的过期时间TTL设置随机,防止大量数据同一时间过期
  • 搭建redis集群将热点数据均匀分布到不用的缓存数据库,提高服务的可用性
  • 缓存业务添加降低限流策略
  • 添加多级缓存(nginx,jvm等)

image-20221207165927327

缓存击穿

缓存击穿问题又称为热点key问题,一个高并发访问并且缓存重建业务较为复杂的Key过期,大量请求访问会在瞬间给数据库带来巨大的压力

image-20221207175832982

解决方案

  • 设置热点数据永不过期

  • 接口限流和熔断降级

  • 加互斥锁

    缺点是线程等待,性能较差

image-20221207232540016

  • 逻辑过期

image-20221207233126773

image-20221207233151021

基于互斥锁方式解决缓存击穿问题

image-20221207234809088

image-20221207234342417

基于逻辑过期方式解决缓存击穿问题

image-20221208110609702

缓存工具封装

优惠券秒杀

全局唯一ID

image-20221209134143378

全局ID生成器

一种分布式系统下用来生成全局唯一ID的工具

image-20221209134432906

image-20221209134920012

ID的组成部分

  • 符号位:1bit,0表示正数
  • 时间戳:31bit,以秒为单位
  • 序列号:32bit,秒以内的计数器,支持每秒产生2^32不同的ID

image-20221209160509685

实现优惠券秒杀下单

image-20221209163723219

库存超卖问题分析

image-20221212093954792

image-20221211224102371

乐观锁

  • 版本号法

通过版本标识数据有没有变化

image-20221212094057983

  • CAS(compare and swap)法

使用库存代替版本

image-20221212094557983

总结

image-20221212132007970

一人一单

image-20221212133704520

集群下的并发安全问题

image-20221212155303894

image-20221212155642926

单体模式

synchronized是通过JVM内部的监视器控制线程的

image-20221212165424079

集群模式

image-20221212165505662

分布式锁

image-20221212165807301

image-20221212165914742

分布式锁简介

分布式锁:满足分布式系统或集群模式下多线程可见并且互斥的锁。

image-20221212170156395

分布式锁的实现

image-20221212170954214

基于Redis的分布式锁

获取锁

image-20221212172255569

释放锁

image-20221212172425586

image-20221212171913934

问题

在setnx和expire语句执行之间,服务发生了宕机,锁的过期时间就添加失败

image-20221212173317049

获取锁

image-20221212173514881

image-20221212173614905

分布式锁误删问题

image-20221213104203635

线程1业务阻塞后锁超时过期,线程2拿到了锁执行业务,此时线程1阻塞业务执行完毕,误删了线程2的锁,导致线程3拿到了锁,执行业务,导致了两个线程同时执行同一个业务

解决方法

获取锁标识并判断是否一致,释放锁验证是否是该线程的锁

image-20221213104353629

image-20221213104836276

改进Redis分布式锁

image-20221213111339279

分布式锁的原子性问题

image-20221213142911540

线程1在获取锁标识判断一致后,将要执行释放锁的操作时,线程1发生由于JVM垃圾回收机制等原因发生了阻塞,锁没有被释放但过期后被释放,线程2执行时获取锁,线程1阻塞结束执行了释放锁的操作,导致线程2的锁被释放,线程3此时获取锁导致了两个线程并行执行。

image-20221213143602261

Redis的Lua的脚本

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

Lua是一种编程语言,它的基本语法:https://www.runoob.com/lua/lua-tutorial.html

基本语法

image-20221213144906636

执行脚本

image-20221213150019865

image-20221213145512377

image-20221213145547488

image-20221213150601394

image-20221213150614674

基于Lua脚本的释放锁

image-20221213153252755

1
2
3
4
5
6
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁 del key
return redis.call('del', KEYS[1])
end
return 0

Java调用lua脚本改造分布式锁

image-20221213160450825

总结

image-20221213164025997

基于Redission的分布式锁优化

基于setnx实现的分布式锁存在下面的问题

  • 不可重入:同一线程无法多次获取同一把锁
  • 不可重试:获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:可以避免死锁,但仍然存在一定的安全隐患。例如业务执行耗时比锁的超时时间长,导致了锁的提前释放等
  • 主从一致性:Redis提供了主从集群模式,主从同步存在延迟。当主节点宕机时,从节点并未同步主节点的锁数据时,会导致多个线程拿到锁的情况,产生安全问题

image-20221213173148132

Redission

image-20221213173415985

官网:https://redisson.org/

使用方法

引入依赖
1
2
3
4
5
6
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redission.version}</version>
</dependency>
配置客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
public RedissonClient redissonClient() {
// 创建配置类
Config config = new Config();
// 添加配置
config.useSingleServer()
.setAddress(SCHEMA_PREFIX + host + ":" + port)
.setPassword(password)
.setTimeout(3000)
.setPingConnectionInterval(30000)
.setDatabase(Integer.parseInt(database));
// 创建Redisson对象
return Redisson.create(config);
}
使用分布式锁
1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过Redisson获取可重入锁
RLock lock = redisLockService.getRLock(REDIS_LOCK_COUPON_HISTORY);
// 尝试获取可重入锁
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
if (!isLock) {
throw new ServiceException("一人只允许购买一张优惠券");
}
try {
...
} finally {
// 释放可重入锁
lock.unlock();
}

Redisson可重入锁原理

image-20221215110558010

image-20221215135354623

获取可重入锁的Lua脚本

image-20221215140201072

释放可重入锁的Lua脚本

image-20221215140328868

总结

利用一个Hash结构记录获取锁的线程和重入的次数,Redisson底层的核心就是Lua脚本

image-20221215153730992Redis分布式锁原理

  • 可重入性:利用ConcurrentMap存储记录线程id和重入次数
  • 可重试:利用信号量和PubSub功能实现等待、唤醒、获取锁失败的重试机制
  • 超时续约:利用WatchDog,每隔一段时间(leaseTime/3),重置超时时间

image-20221215182257133

Redisson的multiLock原理

Redisson分布式锁的主从一致性问题

image-20221215202909112

image-20221215203304279

image-20221215204206587

总结

image-20221216094654492

秒杀优化

异步秒杀思路

并行

image-20221216104022591

image-20221216105527671

image-20221216110149724

image-20221217005201496

使用Lua脚本判断秒杀库存和一人一单状态,保证执行的原子性

image-20221218025955470

总结

秒杀业务的优化思路
  • 利用Redis和Lua脚本完成校验判断和抢单业务
  • 将下单业务放到阻塞队列,利用线程池异步下单
基于阻塞队列的异步秒杀的问题
  • 阻塞队列的内存限制问题(阻塞队列来自JVM)
  • 数据安全问题(阻塞队列中的订单没有被消费完,却发生了重启或宕机等事故)

Redis消息队列

消息队列

消息队列(Message Queue)存放消息的队列,最简单的消息队列模型包括三个角色

  • 消费队列:存储和管理消息,称为消息代理(Message Broker)
  • 生产者:发送消息到消息队列
  • 消费者:从消息队列获取消息并处理消息

image-20221218052232089

image-20221218052650676

Redis提供了三种不同的方式实现消息队列

  • List:基于List结构模拟消息队列
  • PubSub:基本的点对点消息模型
  • Stream:完善的消息队列模型

image-20221218052713338

基于List模拟消息队列

消息队列(Message Queue)存放和管理消息的队列,而Redis的List数据结构是一个双向链表。

image-20221218054419266

image-20221218054947741

image-20221218055421128

image-20221218055432386

image-20221218055445213

image-20221218054241305

基于PubSub的消息队列

PubSub(发布/订阅)是Redis引入的消息传递模型,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有的订阅者都能收到相关信息。

image-20221218162046630

image-20221218162753133

总结

image-20221218162847614

优点
  • 采用发布订阅模式,支持多生产、多消费
缺点
  • 不支持数据持久化
  • 无法避免消息丢失
  • 消息堆积存在上限,超出时数据丢失
  • 消息订阅者停止时,消息发送会丢失

Stream的单消费模式

Stream是Redis引入的一种新的数据类型,可以实现功能更加完善的消息队列

image-20221218163659720

基于Stream发送消息

image-20221218164450534

基于Stream读取消息

image-20221218185944434

image-20221218185713307

image-20221218190349748

image-20221219122859992

image-20221219123108407

image-20221219123158957

当指定起始ID为$时,代表读取最新的消息,如果处理一条消息的过程中,出现了超过1条以上的消息到达队列,则下次获取时,只能获得最新的一条消息,会出现漏读消息的问题。

image-20221219123354171

总结

优点:
  • 消息可回溯
  • 一个消息可以被多个消费者读取
  • 支持阻塞读取

缺点

  • 有消息漏读的风险

    image-20221219130332819

基于Stream的消息队列-消费者组

消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列

  • 消息分流:队列中的消息会分流给组内的不同的消费者,而不是重复消费,从而加快消费处理的速度
  • 消息提示:消费者组会维护一个标示,记录最后一个被处理的消息,由于stream中的小消息本身就支持持久化,当消费者宕机重启后,消费者还会从标示之后读取消息,确保每一个消息都会被消费(避免消息漏读的情况)
  • 消息确认:消费者获取消息后,消息处于pending状态,存入一个pending-list。当处理完成后通过XACK来确认消息,并标记消息为已处理,并从pending-list移除(避免消息丢失的情况)

image-20221219131356818

image-20221219132919466

image-20221219135311350

基于Stream的消费队列实现异步秒杀

达人点评

发布点评笔记

image-20221221135202338

点赞

image-20221222235253214

点赞排行榜

image-20221222235326299

image-20221223000128319

image-20221223000541237

image-20221223000558420

image-20221223222109390

好友关注

关注和取关

image-20230102162103145

共同关注

利用Redis中set数据结构实现共同关注功能。

image-20230102225058098

关注和推送

关注推送也叫Feed流,通过下拉刷新获取新的信息。

image-20230103104020197

image-20230103104103941

image-20230103104231504

拉模式

image-20230103104500644

推模式

image-20230103104655290

推拉结合

image-20230103105213174

image-20230103105225498

推模式实现粉丝推荐

image-20230103121331638

image-20230103122137899

image-20230103135629197

实现关注推送页面的滚动分页查询

按脚标(排名)查询

image-20230103171623943

image-20230103172512500

为了防止score相同的情况下,导致偏移量不准确,所以偏移量应该设置为与上一次分页查询结果中,与最小值一样的元素个数。

image-20230103173157185

image-20230103173437310

附近店铺

GEO就是Geolocation的缩写,代表地理位置,Redis在3.2版本加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度检索数据

image-20230103213604903

image-20230103214000937

常见命令

添加地理数据

image-20230103214310250

image-20230103215308352

image-20230103214435380

计算地理距离

image-20230103214727360

搜索附近地理

image-20230103215157957

附近商铺搜索

image-20230103215914946

image-20230103222141774

用户签到

image-20230105134359071

image-20230105134641859

image-20230105144620835

image-20230105144855288

image-20230105144913949

image-20230105145028242

image-20230105145127510

image-20230105145543477

image-20230105145712440

实现签到功能

image-20230105150653344

统计签到

image-20230106192904777

UV统计

HyperLogLog用法

image-20230107011541737

image-20230107011601838

image-20230107014901129

实现UV统计

配置UV统计拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
public class UVInterceptor implements HandlerInterceptor {
@Value("${redis.key.uv}")
private String UVKey;
@Resource
private RedisTemplate<String, Object> redisTemplate;

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String date = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
Long id = MemberHolder.get().getId();
redisTemplate.opsForHyperLogLog().add(UVKey + date, id);
return true;
}
}

添加拦截器

image-20230107182657453