V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Aruforce
V2EX  ›  程序员

蛋疼的 Redis 和需求...我想问下各位都是怎么实践 Redis 和 Mysql 数据一致性的或者怎么骂回去?

  •  
  •   Aruforce · 2020-12-17 12:17:08 +08:00 · 9036 次点击
    这是一个创建于 1429 天前的主题,其中的信息可能已经有所发展或是发生改变。

    1.目前基本技术栈

    1. SpringMVC
    2. Mybatis
    3. Redis

    2.基本代码实践思路

    1. 数据更新完全在 Mysql 事务之中 不会读取更新缓存
    2. 现在 Redis 只是用来做接口缓存(数据有有效期,Key 根据 API 入口参数建立,value 就是 Mysql 查询的结果)

    3. 目前提出的需求及存在的问题

    1. Mysql 数据更新之后 要求全部的接口必须和数据库一致
    2. 现在的接口返回的数据有很多连表查询,所以 mybatis 全局缓存并不好使。。同一个 namespace 的连表查询结果在表数据更新后缓存会被删除但是其他的 namespace 不行。。所以不好使
    3. 老板不懂开发
    4. 如果能做给的实践也不算长

    4.我该怎么处理?

    1. 无法完成缓存强一致性,完成不了需求。。这个不太好。。不过有好的说服的理由的话我也试试
    2. 统计在任何一个表数据更新之后需要删除关联查询接口的缓存? 这个工作量太大了
    3. 或者各位给我们一个好的 Redis 缓存实践? Redis 做接口缓存自我感觉也不太好

    我倾向与 1 和 3
    一个是不做。。
    一个是 能争取实践或者我能学点东西。。。

    2 太他么的蛋疼了

    第 1 条附言  ·  2020-12-17 15:38:58 +08:00

    可能有人看不懂问题 我再补充一点吧...拿着订单流程来举例

    1. 下面存在两套业务接口

    1. 一个套接口是订单数据的更新
    2. 一个套接口是面向各业务系统的用户看到订单相关的业务数据 这些是需要连表查询出来的(这些接口结果数据被缓存)

    2. 需求

    1. 订单数据更新完成的话,要求关联的业务系统看到的数据 必定是数据库的实时数据(如果有更新,则再次查看不命中缓存而是去数据库读取,必须考虑到到更新和查看之间没有时间差这种极限情况,也就是缓存和数据库数据的强一致性)

    3. 如何实现需求及各自的问题

    1. 对老代码改动较小的方案及其问题

    一个表数据更新之后,把这个表所有的关联查询结果全部清除
    问题:

    1. 链表查询的的缓存结果在不同的namespace中,order表的更新只能保证order namespace的数据清空,其他的不行

    2. 一个近乎重构的方案:

    开启mybatis的全局缓存,禁用连表查询,可以屏蔽上面那个方案的问题
    问题:

    1. 老接口改动很大
    2. 在事务之中 mysql 和redis 还有提交的可能单独失败的问题
    3. 链表查询改到服务内完成 。。可能是错误的(因为这期间 已基本处理过的数据又可能发生了变更,没有mysql的 MVCC )...
    71 条回复    2020-12-18 19:54:39 +08:00
    EminemW
        1
    EminemW  
       2020-12-17 12:25:08 +08:00 via iPhone
    弄个定时任务主动刷不就好了,如果是手动修改数据的,就弄个刷缓存按钮
    jones2000
        2
    jones2000  
       2020-12-17 12:26:40 +08:00
    redis 存储的时候增加查询的数据库表名, 数据更新了, 把 redis 里面涉及到这个表的缓存全部删了.
    Aruforce
        3
    Aruforce  
    OP
       2020-12-17 12:28:40 +08:00
    @EminemW
    你走的还是 2 这个思路..不是不行 就是工作量大...
    而且 以后还或有新的连表查询加入....
    老代码还要跟着改...
    ningmengmao
        4
    ningmengmao  
       2020-12-17 12:29:08 +08:00 via Android
    spring cache 应该可以吧
    Aruforce
        5
    Aruforce  
    OP
       2020-12-17 12:36:53 +08:00
    @jones2000 这相当于一个把 关联数据的删除不和老业务代码合在一起了 可以做成一个切面逻辑只用把变更的表名弄出来就行然后直接扫描 redis key 然后删除...这应该算是 2 的变种
    sagaxu
        6
    sagaxu  
       2020-12-17 12:38:37 +08:00 via Android
    先撤掉 redis,mysql 内存加大点,看看性能是不是真的扛不住
    Aruforce
        7
    Aruforce  
    OP
       2020-12-17 12:42:27 +08:00
    @sagaxu mysql 调整过。。模拟线上流量 压测过了不行... 如果不对接口拦截的服务全是超时和 500 傻的
    cz5424
        8
    cz5424  
       2020-12-17 12:45:18 +08:00 via iPhone
    在 db 层做 hook,查询优先命中缓存,修改删除缓存(没写过 java
    cz5424
        9
    cz5424  
       2020-12-17 12:46:04 +08:00 via iPhone
    多表查询要拆分查询,这个比较麻烦
    k9982874
        10
    k9982874  
       2020-12-17 12:46:17 +08:00
    根据经验 redis 缓存做到接口级别,记录级别做不了。你做的再好,一个批量条件 update 或者 delete 就废了。
    除非你把每次 update 受到影响的记录都重新查出来写入 redis,不过这本身就是个性能问题。

    同等楼下大佬给个解答。
    sadfQED2
        11
    sadfQED2  
       2020-12-17 12:47:24 +08:00 via Android   ❤️ 5
    我司方案,所有接口数据直接读 redis 。

    有一个 worker 监听 mysql binlog,根据 mysql binlog 生成 redis 中的新数据
    sagaxu
        12
    sagaxu  
       2020-12-17 12:51:17 +08:00 via Android
    @Aruforce 强一致性基本不太现实,无论是 write back 还是 write through,都有一定的 delay,扫描一次 key 可能就要几十秒了,并不比超时更快。
    Aruforce
        13
    Aruforce  
    OP
       2020-12-17 12:54:03 +08:00
    @cz5424 你说的这个就是
    > 可以做成一个切面逻辑只用把变更的表名弄出来就行然后直接扫描 redis key 然后删除.。。
    这一段... 基本原理
    Aruforce
        14
    Aruforce  
    OP
       2020-12-17 12:56:02 +08:00
    @sadfQED2 binlog 转到 redis 也是有延时的。。。而且对以前的接口都要改。。。不现实。。。
    sampeng
        15
    sampeng  
       2020-12-17 12:57:25 +08:00 via iPhone
    什么毛病,自己实现的技术方案满足不了需求不去想办法解决,而是选择怼回去,跳槽,消极怠工?新一代程序员这么不堪了?
    des
        16
    des  
       2020-12-17 12:58:07 +08:00
    不用连表查询,在内存中进行连接呢?
    Aruforce
        17
    Aruforce  
    OP
       2020-12-17 12:59:28 +08:00
    @sadfQED2 而且连表查询 会十分胃疼
    Aruforce
        18
    Aruforce  
    OP
       2020-12-17 13:00:56 +08:00
    @des 不可能的。。。已经有很多连表查询了 这些不可能有时间重写
    Aruforce
        19
    Aruforce  
    OP
       2020-12-17 13:03:51 +08:00
    @sagaxu 如果没有 更好的实短时间完成的方案的话。。。我先就先推降低缓存有效期了。。。再不行就是做 mapper 的 hook 再不行就禁止写连表查询。。把连表操作转到内存里去了
    Aruforce
        20
    Aruforce  
    OP
       2020-12-17 13:05:49 +08:00   ❤️ 1
    @sampeng 你要是能给解决方案就给。。。别一上来的就这指责别人的态度。。就像个老娘们似的
    sampeng
        21
    sampeng  
       2020-12-17 13:06:03 +08:00 via iPhone   ❤️ 3
    因为做的时候就没考虑高一致性的高并发。高并发和事务是相对互斥的实现方案。要想达到完美,需要对事务和缓存结构做非常好的设计。一看你说拿 api 的 key 做缓存就知道再项目开始就没设计缓存架构。这是技术债务,找老板要时间填坑或者自己加班填坑。

    我司有一个服务就是这么实现的,当然没有事务,但是性能差的发指。20 台机器吃 30 万日活。然后有互联网经验的做了数据级别缓存后,3 台即可。还是为了高可用…不然一台都够了。

    程序员的价值是解决问题,不用问别人,你自己是有方案的。一个一个试,这才是最有价值的地方。搞成了,这个坑你再不会进去,也有极其丰富的经验。问别人,谁能手把手教你呢
    sampeng
        22
    sampeng  
       2020-12-17 13:15:42 +08:00 via iPhone   ❤️ 1
    @Aruforce 方案就是不要连表,redis 做分布式事务。所有子查询都是 redis 缓存,所有写入先写 redis 。工作量肯定很大,你不会选
    gadsavesme
        23
    gadsavesme  
       2020-12-17 13:22:21 +08:00   ❤️ 1
    一般就是两种解决方案啊,延时双删,还有就是订阅 mysql 的 binlog 。不过极限情况下不同步肯定会有,要强一致那就加锁呗,数据同步过程中就等待好了,这个时间就毫秒级,应该也不大会影响。
    micean
        24
    micean  
       2020-12-17 13:30:22 +08:00
    调整缓存:主键 = 行数据
    调整接口:查询数据库返回需要数据行的主键,再从缓存中取回行数据

    等数据库扛不住再说
    dawniii
        25
    dawniii  
       2020-12-17 13:35:17 +08:00
    @sampeng 总感觉很多极限情况下,会有问题。比如 redis 写成功了,mysql 还没 commit 呢,机器挂了,redis 不会自动回滚吧,这时候 redis 的就是脏数据了,感觉总是要牺牲点什么。
    sampeng
        26
    sampeng  
       2020-12-17 13:38:28 +08:00 via iPhone
    @dawniii 我刚说了,取消数据库事务实现。全部靠程序控制。
    dawniii
        27
    dawniii  
       2020-12-17 13:46:43 +08:00
    @sampeng 不管怎么实现,都需要面临双写的情况吧。极限情况可能会出现,其中一个成功,另一个失败。然后再弄个分布式的事务去重试或者回滚(逆操作)吗?但是在你程序回滚的过程中,确实有脏数据了。
    dawniii
        28
    dawniii  
       2020-12-17 13:49:03 +08:00
    @dawniii 这算是最终一致性?
    dswyzx
        29
    dswyzx  
       2020-12-17 13:54:50 +08:00   ❤️ 2
    一直以为 redis 的缓存用法是缓存热点数据,妹想到现在都是把 redis 当 mysql 用了
    JasonLaw
        30
    JasonLaw  
       2020-12-17 14:12:03 +08:00
    "Turning the database inside out with Apache Samza" by Martin Kleppmann - YouTube

    kkkkkrua
        31
    kkkkkrua  
       2020-12-17 14:15:45 +08:00
    如果是用的 spring cache 组件的话,可以在更新的时候删除 cache
    caryqy
        32
    caryqy  
       2020-12-17 14:17:02 +08:00
    看你的第一个需求需要 Mysql 数据更新之后 要求全部的接口必须和数据库一致


    更新 mysql 时先把 redis 中此类数据锁住,此时进来的请求都等待,mysql 更新完之后更新 redis 中数据完成之后删除 redis 锁, 请求返回最新的缓存数据,如果此期间 mysql 或 redis 任何一端挂掉了 /服务挂掉了,请求端得到错误响应 /超时之后需要重新请求, 数据抄送一份到 kafka 中每天的定时任务去校验当天 /时数据
    caryqy
        33
    caryqy  
       2020-12-17 14:22:45 +08:00
    希望别一顿操作之后和这个类似 https://www.v2ex.com/t/735360 🐶
    Jrue0011
        34
    Jrue0011  
       2020-12-17 14:23:40 +08:00
    @dawniii 我感觉他的意思应该是写的话不操作数据库只写缓存,通过某种方式将缓存的变化异步更新到数据库
    LJ2010
        35
    LJ2010  
       2020-12-17 14:26:02 +08:00
    什么是缓存? 重点缓解存储的压力, 而不是存储本身,既然是缓存必然存在数据不一致时差的问题,而且缓存目的是把热数据降温,而不是存储所有数据,换个角度就没必要纠结数据一致性的问题,什么样的数据需要一致性?我的观点涉及钱的需要,其他的不需要,所以没必要纠结数据瞬时的一致性,所以你一定要坚持 观点 1,否则就骂街:)
    DoubleShut
        36
    DoubleShut  
       2020-12-17 14:26:31 +08:00
    读 binlog 进行同步
    AA5DE3F034ACCB9E
        37
    AA5DE3F034ACCB9E  
       2020-12-17 14:26:53 +08:00
    不可能三角吗
    pengliu
        38
    pengliu  
       2020-12-17 14:26:53 +08:00
    要求数据实时就不要加缓存,加了缓存肯定会有数据不一致,根据业务需求再定技术方案
    liudaolunhuibl
        39
    liudaolunhuibl  
       2020-12-17 14:33:40 +08:00
    @sadfQED2 想到过这种方案,但是如果是连表的缓存怎么办呢?貌似 binlog 都是单表的
    hhyyd
        40
    hhyyd  
       2020-12-17 14:41:39 +08:00
    不是很明白楼主的问题。spring 的 cache 组件,通过 cacheEnable 和 cacheEvict 注解可以很方便的控制 redis 缓存啊。 麻烦的地方是,查询涉及到多张表,修改每张表的时候都需要删除查询时涉及到的 key,可能需要你设置一组合理的 key,来解决这个问题。
    lijialong1313
        41
    lijialong1313  
       2020-12-17 14:47:23 +08:00
    读写 redis,然后用 worker 丢到数据库里。
    moonblog
        42
    moonblog  
       2020-12-17 15:20:52 +08:00
    这不就是 spring redis cache 做的事情吗
    service 层,几个注解解决的事
    query 后(由于是 service 层,是否 joint 表随意),缓存到 redis
    update 后,evict redis cache
    dawniii
        43
    dawniii  
       2020-12-17 15:33:46 +08:00
    @Jrue0011 有可能是的。这种方法,感觉不是后台全场景都那么适用吧,后台一般有各种事务和查询条件。。。如果不是全数据都在 redis 里,在后台查询的时候也挺恶心,热的从 redis 查,冷的从数据库查?
    Jrue0011
        44
    Jrue0011  
       2020-12-17 16:07:04 +08:00
    mybatis 缓存是什么情况,你们是用 redis 实现 mybatis 的二级缓存吗?如果上 mybatis 本地缓存的话,一个服务器的修改也没法清除另一个服务器上的 mybatis 缓存啊
    securityCoding
        45
    securityCoding  
       2020-12-17 16:12:08 +08:00
    @dswyzx 233 ,就是一个缓存策略问题 .
    cache aside , write/read through 最大区别是 write through 会代理数据操作 , 业务层面直接跟缓存组件交互.
    mtrec
        46
    mtrec  
       2020-12-17 18:02:41 +08:00 via Android
    cap,cache aside pattern
    读 缓存有直接用 没有就查数据库然后更新缓存
    写 写完 db 清对应的缓存
    yzbythesea
        47
    yzbythesea  
       2020-12-17 18:10:30 +08:00
    说实话 QPS 没上 10,真的不用 redis 缓存。
    Vegetable
        48
    Vegetable  
       2020-12-17 18:19:43 +08:00
    @sadfQED2 这个读写分离可以的
    laminux29
        49
    laminux29  
       2020-12-17 18:26:46 +08:00   ❤️ 2
    上面一堆人还没搞清楚原因就给建议..

    1.Mysql 支持事务但性能不够,Redis 性能够但不支持事务。

    2.Redis 性能之所以够用,本质是因为相对于 Mysql,Redis 砍掉了数据安全与事务功能,这样全跑在内存里,又不要考虑事务,速度不快才怪。

    3.题主的需求:Mysql 数据更新之后,要求 Redis 必须和数据库一致,本质上是要给 Redis 增加事务,还要让 Redis 接受 Mysql 的控制,这是不现实的。

    ================
    几种方案:

    1.Mysql 数据只做新增,不查不改不删,然后推送到 Redis,Redis 做只查,然后允许 Mysql 与 Redis 存在短期内的不一致。这是大厂,包括谷歌的标准玩法。

    2.有钱能增加机器,并且业务支持并行写入或并行读取,则可以根据业务,把系统设计为对并行写入优化但会增加读取时间,或者设计为对并行读取优化但会增加写入时间。

    3.非常有钱,直接上 Oracle 最新版,支持内存表,虽然没 Redis 快,但比 mysql 快得多,还支持事务。
    sadfQED2
        50
    sadfQED2  
       2020-12-17 18:50:08 +08:00 via Android
    @liudaolunhuibl 1.并不是缓存表的数据,而是像楼主那样缓存接口数据,每次表更新重新掉接口拿
    2.我们没有连表的操作
    sadfQED2
        51
    sadfQED2  
       2020-12-17 18:52:55 +08:00 via Android   ❤️ 1
    @Aruforce 你连 binlog 转 redis 这点延时都忍受不了,那你就不能考虑缓存了啊
    libook
        52
    libook  
       2020-12-17 18:53:54 +08:00
    写程序难题 Top2:起名和维护缓存。

    把缓存和数据库的操作封装在一起,对业务功能仅提供统一的接口,接口被调用后内部管理缓存和数据库的操作。

    基本思路就是有读的话直接去 Redis 里读,有写的话先删缓存,再写数据库。

    可以在写入的时候使用事务,确保写入的时候数据库里的值没有被其他应用实例修改,如果遇到了就尝试二段提交。

    考虑到缓存刚刚被删除就有可能有读请求进来,为了确保一致性,可以在删缓存之前在数据库记录中加一个锁(类似排他锁),等更新完再解锁。

    数据库层级的事务在引入微服务思想和各种中间件、数据库之后,会有较大的局限性,此时就要更多考虑分布式事务方案。
    corningsun
        53
    corningsun  
       2020-12-17 19:25:25 +08:00 via iPhone
    难点是什么时候删除。
    表和接口不多的话,维护所有表和查询接口的关联关系。
    然后哪张表更新了,就删除对应查询接口的缓存即可。
    kaneg
        54
    kaneg  
       2020-12-17 21:44:07 +08:00 via iPhone
    你既要 mysql 的实时存储,还要 redis 的高速缓存,这是鱼翅和熊掌不可兼得的。
    除非你的接口非常简单和少量,明确知道哪个操作会造成缓存和数据库不一致,然后让其失效。
    理想要落地必然要做取舍,否则追求的结果必然是镜中花,水中月。
    vindurriel
        55
    vindurriel  
       2020-12-18 01:00:56 +08:00 via iPhone
    读取全部走 redis key 分两种:列表和单行,其中列表的 values 能对应到单行的 keys,对应数据库每一行的 pk

    写入单行:在 mysql transaction 的最后一步写 redis 如果写入不成功 db 回滚 redis 不是集群的话就强一致了

    写入列表:batch 或者 stream 进行联表查询 只能最终一致
    wangritian
        56
    wangritian  
       2020-12-18 02:03:16 +08:00
    赞同 21 、29 、35L
    缓存是用在数据层的,而不是接口层,你自己也感觉到了
    其次也不能滥用,像复杂条件查询或是 join 查询等等,应该去优化索引和提高 mysql 硬件配置,然后考虑分表
    不建议你在错误的方案下解决问题
    black11black
        57
    black11black  
       2020-12-18 03:00:56 +08:00 via Android
    @sadfQED2 所以一次 redis 请求还要附带一个 binlog 检查操作?感觉有点本末倒置啊
    YouLMAO
        58
    YouLMAO  
       2020-12-18 08:06:23 +08:00 via Android
    删除 Redis,MySQL 换成 cloud spanner
    gosansam
        59
    gosansam  
       2020-12-18 09:48:01 +08:00
    @laminux29 请问数据的更新是通过 binlog 完成的吗?
    seth19960929
        60
    seth19960929  
       2020-12-18 10:16:52 +08:00
    我觉得你可以找一个支持 tag 方式的缓存库(类型 laravel 的 tags cache)
    然后你继续在 API 层面做缓存.
    针对 API 写好 tag, 比如 API 这样:
    接口 1 依赖 users, points 表, 就给这个接口增加 users, points 标签(用 model 的名字更好反射)
    接口 2 依赖 points 表, 给这个接口打上 points 标签

    当 users 表发生变化, 直接清空 users tag 的接口 1
    当 points 表发生变化, 直接清空 points tag 的接口 1, 2
    cuiweieee
        61
    cuiweieee  
       2020-12-18 10:19:03 +08:00
    修改数据用 mq 异步通知,mq 消费之后刷 redis
    RedBeanIce
        62
    RedBeanIce  
       2020-12-18 10:48:28 +08:00
    @hhyyd #38 请问一个问题,spring cache 可以插入不是 string 类型的吗,,因为我看到底层调用的是 sring,不知道是否可以插入 list set hash zset
    iceneet
        63
    iceneet  
       2020-12-18 11:14:55 +08:00
    感觉用 redis 缓存要求强一致性有点怪
    ZSeptember
        64
    ZSeptember  
       2020-12-18 11:17:08 +08:00
    缓存和强一致冲突。
    v2orz
        65
    v2orz  
       2020-12-18 11:39:08 +08:00
    换一个内存数据库代替 mysql,去掉 redis
    zhangfeiwudi
        66
    zhangfeiwudi  
       2020-12-18 12:17:49 +08:00
    我最近在做这一块
    zhangfeiwudi
        67
    zhangfeiwudi  
       2020-12-18 12:18:49 +08:00
    @zhangfeiwudi 主要就是做个触发式缓存,db 更新了 发 q 或者监听胖消息通知 cache , 然后所有 cache 收口到一个团队来吐出 比如说我们有 innerapi 接口
    xiaotanyu13
        68
    xiaotanyu13  
       2020-12-18 14:03:02 +08:00
    把读多写少被连表又多的数据常驻 redis,放弃 @cache,自己写代码维护这类数据;
    连表查的数据的话就看情况了,如果要查的数据能从 redis 中取到,就可以少一次连表查询(指的是部分数据直接取 redis 中的数据就够了),然后剩余的数据从数据库查询
    更新的话 先更新 mysql,再更新 redis,如果 redis 更新失败,直接抛异常 让 mysql 回滚
    sakasaka
        69
    sakasaka  
       2020-12-18 14:05:28 +08:00
    mysql 性能没有这么不堪,加入缓存必然会有一致性问题,使用各种监听器同步也会增加系统复杂度,如果业务真的对性能和一致性要求高,建议改用别的 nosql 数据库,这个需要结合具体业务。
    linoder
        70
    linoder  
       2020-12-18 15:47:15 +08:00
    要不 …… 了解下 TiDB ?
    dawniii
        71
    dawniii  
       2020-12-18 19:54:39 +08:00
    @vindurriel
    @xiaotanyu13

    写入单行:在 mysql transaction 的最后一步写 redis 如果写入不成功 db 回滚 redis 不是集群的话就强一致了

    ================================================================================

    先更新 mysql,再更新 redis,如果 redis 更新失败,直接抛异常 让 mysql 回滚

    ==========================================================

    极限情况下,redis 成功,mysql 还没 commit 挂了 - -
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5468 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 44ms · UTC 06:00 · PVG 14:00 · LAX 22:00 · JFK 01:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.