事件弹窗配置缓存设计

需求背景

教育中心需要从控制台CMS端为APP配置一些通用的活动弹窗,事件弹窗,在用户打开某些指定的页面时按照固定的规则将配置的事件弹出屏幕,启动APP就需要调用所有配置的弹窗所以尽量要求高性能,越快越好。

一、通用简单设计

在控制台需要新增,修改,删除,查询列表,列表项上下移动与置顶操作,数据量在500条以内的情况,简单不考虑多个运营人员并发操作的情况下一般使用下面的设计,对DB的每一次修改都会实时将整个表中有效数据同步到Redis缓存中,使用一个大的JSON列表缓存,由于教育中心是不允许APP端直接从Redis查到DB的(Read_DB 用虚线),在一般系统中是允许的。

简单设计

对于这种这种设计存在一下几个缺陷

  1. 数据量被限制

当key存储的数据较大,或者数据结构等等过于复杂,redis在处理这个数据的时候,延迟就会增高,进而也会阻塞队列中的其他请求,最终导致整个实例的查询时延都会增高。此外,在并发较高时,redis的带宽占用也会增高,假设单key大小为700kb时,并发为1024,那么每秒产生的流量就有接近700M,这对服务器的IO压力是十分巨大。

  1. 热点Key问题

事件弹窗配置属于非用户态,对于非用户态的缓存,并发环境极有可能出现热点Key的问题,在一般的通用系统里面允许查询DB的话,就会造成DB性能影响,当然在咱们教育不允许直接查询DB,DB影响可以屏蔽掉。

  1. 颗粒度问题

控制台的多种修改操作,每一次修改需要将整个列表数据全部查询,在内存操作后再缓存,颗粒度太大。

  1. DB与缓存数据一致性问题

可能存在各个地区的运营人员对事件弹窗同时并发做不同的操作,比如两个更改操作同时过来,写入DB,但是在两个操作的会话空间里面获取整个列表数据时,只能看到自己更新的数据,与其他并发事务操作隔离。每个并发修改DB操作,与写入缓存一般都在同一个事务,导致每个并发写入缓存的数据都会不全,只能缓存当前本线程修改的数据,其他并发线程修改的数据缓存不了。

二、优化缓存设计

考虑到线网环境(生产环境)存在控制台并发,缓存颗粒度,数据一致性问题需要对上面的设计做优化,设计如下图:
在这里插入图片描述

1、防冲刷
对控制台参数限制,每一次调用生成唯一的requestId,DB表使用唯一性约束字段接受,当存在网络延时重提新增操作,会抛出数据重复异常,从而保证每条数据的唯一性,降低脏数据出现的可能

2、缓存数据打散
每个弹窗事件都有优先级排序,考虑将每个有效事件eventId以有序集合Zset方式缓存,因为他天然支持排序,获取这个集合时无需再次内存排序。同时将以事件eventId为维度将整个有效列表打散成若干份数据缓存。这样就可以实现数据修改操作不需要对整个大列表进行内存操作,每修改一项事件弹窗配置只需要单独修改Zset的指定成员和某一项事件缓存-。

3、分布式锁
在上面的优化方案图中控制台同一个线程操作包含:
@1:修改DB数据
@2、修改Redis缓存Zset有序集合,单数据内容修改无需此操作
@3、修改指定的事件弹窗配置缓存
这三个步骤在控制台并发操作下数据一致性问题是极有可能出现的,因此在这个三步操作除了需要添加事务之外还需要添加分布式锁。这个锁可以使得这几个操作在系统维度来说是原子的整体(虽然不支持Redis异常回滚,但是Redis出现异常的情况概率极低),在并发维度上来说控制台的各类数据修改操作只要使用的相同的分布式锁就保证了修改操作串行化,从而使得DB与Redis数据保持一致。当然在使用分布式锁时需要注意预算这个原子操作内的耗时,尽量不要含有耗性能的操作,同时分布式锁也要自动设置过期时间。

三、拓展思考

1、为什么并发写操作只能看到当前线程与其他线程新增数据,而看不到其他线程修改的数据?

每一个线程操作DB都会与DB连接层建立自己独立的回话。而我们在代码中修改数据添加Spring的注解@Transaction,对于事务的隔离级别都是使用数据库默认的default
针对不同的数据库有不同的默认值一般有下面两种
ORACLE 默认的隔离级别(READ_COMMITED = 读已提交)
MySQL默认的隔离级别(REPEATABLE_READ = 可重复读)
一次mysql中其他线程对数据的修改是看不到的。

2、为什么习惯上要先写DB,在写缓存?

这种问题还是从事件弹窗配置看看这个用户与控制台对数据的操作流程才能既进一步分析,在一般的系统设计是APP端允许查询DB的,所以Redis到DB的查询也加上。
如下图:
在这里插入图片描述
反过来思考控制台先写缓存,再写DB会出现的问题,
在某个时刻指定的事件缓存过期,用户端读操作(user_Read)与控制台写操作(const_Write)同时并发过来到缓存那么会出现如下情形:

(1)const_Write进行写操作,删除缓存

(2)user_Read查询发现缓存不存在

(3)user_Read查询DB得到旧值

(4)const_Write将新值写入数据库

(5)user_Read然后将旧值写入缓存

这里user_Read查询DB的速度一般都是要const_Write写DB的操作要快,所以上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

写的不好之处欢迎吐槽我改,下一讲继续缓存一致性问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值