美团财务科技后端一面:如何保证数据一致性?延时双删第二次失败如何解决?

发布于:2024-04-19 ⋅ 阅读:(25) ⋅ 点赞:(0)

更多大厂面试内容可见 -> http://11come.cn

美团财务科技后端一面:项目内容拷打

美团财务科技后端一面:项目相关面试题,主要包含 Zset、延时双删失败重试、热点数据解决、ThreadLocal 这几个方面相关的内容

由于前几个问题是对个人项目的介绍,根据自己项目进行回答即可

是个人进行项目开发的吗?

如何进行系统设计的?

项目是否已经上线?

在项目开发中遇到的难点?如何解决?

项目开发过程中有什么收获?

Zset 底层数据结构,为什么要用Zset?

Zset 的底层实现是:压缩列表 + 跳表

什么时候使用压缩列表?

  • 有序集合保存的元素个数小于 128 个
  • 有序集合保存的所有元素成员的长度都必须小于 64 字节

否则使用跳表

跳表在 Redis 中的作用就是作为有序集合类型的底层数据结构,

跳表中每个节点保存着其他节点的指针,高层的指针越过的元素数量大于等于低层的指针,因此在跳表中,在查找元素时可以一次跳过多个节点,当找到大于或等于目标元素的节点后,再使用普通指针开始移动(可以向后移动,也可以向前移动,跳表含有前边节点的指针)寻找目标元素,跳表可以在 O(logn) 的时间内遍历跳表

跳表结构图:

1697874023019

至于为什么要用 Zset 结合自己的项目说的合理即可

你如何保证缓存与数据库一致性,延时双删的第二次删除缓存失败了呢,如何解决?

这个要看延时双删具体如何实现了,先说一下它的流程:

用户发起更新数据的请求
1、删除数据对应缓存
2、更新数据库
3、休眠一段时间(等待数据库更新完毕之后)
4、再次删除数据对应缓存

这里要休眠一段时间等待第二次删除缓存,因此可以通过 Redisson 的延时队列来做,将第二次删除缓存的信息发送到延时队列中,等待被消费

在消费的时候可以去对失败操作进行处理,从延时队列中取出消息进行消费,如果失败了,可以进行指定次数的重试,如果只是因为网络抖动失败,那么一般重试就可以成功消费了,但是如果是 Redis 出现了问题,那么这一段时间内重试可能都还会存在问题,因此要考虑将失败的消息给放在数据库中存储起来,再定时进行消费对失败的消息进行补偿(不过 Redis 宕机的概率很小,一般不会发生)

上边是个人思路,如有不足欢迎补充

Redis 如何存储热点数据?

热点数据就是某些数据访问频率过高

首先对于热点数据要先可以检测到,检测之后再看具体如何解决

检测的话,可以使用京东零售开源的 hotkey 框架,是一个轻量级通用热 key 探测中间件,可以快速将热数据推送到 JVM 内存,减少大量请求对下游服务、Redis、MySQL 的冲击

那么热点数据的存储的话,可以存储一份到 JVM 内存中,进一步提升数据的查询性能,可以选用 Caffeine (Java 高性能缓存库)来管理 Redis 的热点数据,Caffeine 是基于 Google Guava 改进的,因此 Caffeine 的性能表现、缓存命中率相对来说更好

项目为什么要用 ThreadLocal?ThreadLocal 如何保证线程隔离的?

项目为什么要用 ThreadLocal ,这个可以基于 ThreadLocal 的特性来回答

在多线程环境下,有些数据是你当前线程上下文中要使用到的,但是如果每次调用方法都将这些数据传递过去,或者每次使用的时候都重新过去是比较麻烦的,并且也会带来一定的时间开销,因此可以存储在 ThreadLocal 中,在当前线程的上下文中进行使用

这样在用到数据的时候,直接从 ThreadLocal 中取出来,既保证了线程之间的数据隔离性,又保证了较好的性能(不必重复去获取)

ThreadLocal 如何保证线程隔离的?

如下图,在每一个线程 Thread 中都会定义一个 ThreadLocalMap 属性,该属性就存储 ThreadLocal 所对应的值,那么每个线程中都有一份 ThreadLocalMap 的变量,以此来实现线程隔离

ThreadLocal保证线程之间数据隔离

扩展:ThreadLocal 正确使用姿势

ThreadLocal 的使用规范:将 ThreadLocal 变量定义为 private static final ,并且在使用完,记得通过 try finally 来 remove 掉,避免出现脏数据

扩展:ThreadLocal 内存泄漏问题

ThreadLocal的内存泄漏问题

这里假设将 ThreadLocal 定义为方法中的局部变量,那么当线程进入该方法的时候,就会将 ThreadLocal 的引用给加载到线程的栈 Stack 中

如上图所示,在线程栈 Stack 中,有两个变量,ThreadLocalRef 和 CurrentThreadRef,分别指向了声明的局部变量 ThreadLocal ,以及当前执行的线程

而 ThreadLocalMap 中的 key 是弱引用,当线程执行完该方法之后,Stack 线程栈中的 ThreadLocalRef 变量就会被弹出栈,因此 ThreadLocal 变量的强引用(ThreadLocalRef)消失了,那么 ThreadLocal 变量只有 Entry 中的 key 对他引用,并且还是弱引用,因此这个 ThreadLocal 变量会被回收掉,导致 Entry 中的 key 为 null,而 value 还指向了对 Object 的强引用,因此 value 还一直存在 ThreadLocalMap 变量中

由于 ThreadLocal 被回收了,无法通过 key 去访问到这个 value,导致这个 value 一直无法被回收, ThreadLocalMap 变量的生命周期是和当前线程的生命周期一样长的,只有在当前线程运行结束之后才会清除掉 value,因此会导致这个 value 一直停留在内存中,导致内存泄漏

当然 JDK 的开发者想到了这个问题,在使用 set get remove 的时候,会对 key 为 null 的 value 进行清理,使得程序的稳定性提升。

当然,我们要保持良好的编程习惯,在线程对于 ThreadLocal 变量使用的代码块中,在代码块的末尾调用 remove 将 value 的空间释放,防止内存泄露。

ThearLocal 内存泄漏的根源是:

由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏

ThreadLocal 正确的使用方法:

  • 每次使用完 ThreadLocal 都调用它的 remove() 方法清除数据
  • 将 ThreadLocal 变量定义成 private static final,这样就一直存在 ThreadLocal 的强引用,也能保证任何时候都能通过 ThreadLocal 的弱引用访问到 Entry 的 value 值,进而清除掉