redis-缓存和数据库写入不一致问题

本文探讨了Redis中缓存与数据库不一致的问题及解决方案。介绍了CAP理论在实际场景中的应用,解释了为何选择删除缓存而非更新缓存。为解决数据不一致,提出了通过JVM内部队列实现串行化操作的策略,以避免高并发下的数据冲突和读请求超时,同时强调了在多服务实例部署时的请求路由和热点问题。

一.解决不一致两种方法:

  1. CAP理论(Cache Aside Pattern):缓存+数据库读写的模式

    1.读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
    2.更新的时候,先更新数据库,然后再删除缓存。
    
  2. 请求和写请求串行化,串到一个内存队列里去

     串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求
    

二.CAP为什么是删除缓存,而不是更新缓存

原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。

比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。

另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?

举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。

其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。

三.CAP可能会出现上述的数据库+缓存不一致的情况

数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了

解决方案如下:

更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。

一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。

这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。

待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。

如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。

高并发的场景下,该解决方案要注意的问题:
  • 读请求长时阻塞
    由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。

    该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。

    另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压 100 个商品的库存修改操作,每隔库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致读请求的长时阻塞。

    一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。

    如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。

    其实根据之前的项目经验,一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的 QPS 能到几百就不错了。

  • 读请求并发量过高
    这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。

    但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。

  • 多服务实例部署的请求路由
    可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上
    比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。

  • 热点商品的路由问题,导致请求的倾斜
    万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。

<think>我们正在使用redis-cli向Redis数据库写入键值对数据。根据引用内容,我们可以使用SET命令来写入单个键值对,或者使用MSET命令来写入多个键值对。同时,我们还可以指定数据库、设置过期时间等。 步骤: 1. 连接到Redis服务器:可以使用redis-cli命令,根据需要指定主机、端口、密码数据库。 2. 使用SET命令写入单个键值对,或者使用MSET写入多个键值对。 根据引用[1][3]中的示例,我们可以有以下几种方式: 方式一:交互模式 - 打开终端,输入redis-cli(如果Redis服务器在本地且端口为6379,且无需密码) - 然后使用SET命令: ``` SET key value ``` 方式二:非交互模式(在命令行中直接执行) - 在终端中直接输入: ``` redis-cli SET key value ``` 如果需要指定主机、端口、密码数据库,可以添加选项: - -h:主机地址 - -p:端口 - -a:密码 - -n:数据库索引 例如,连接到远程服务器192.168.1.100,端口6380,密码为mypassword,数据库索引为2,并写入键值对(name: Alice): ``` redis-cli -h 192.168.1.100 -p 6380 -a mypassword -n 2 SET name Alice ``` 注意:在非交互模式下,如果密码包含特殊字符,可能需要用引号括起来,或者使用单引号。 另外,引用[3]中还提到,可以指定ip、端口、密码并清理redis缓存(即清空当前数据库),但这里我们只需要写入操作。 写入多个键值对(使用MSET): ``` redis-cli MSET key1 value1 key2 value2 ``` 例如: ``` redis-cli MSET name Alice age 30 ``` 如果需要设置过期时间,可以使用SETEX(针对字符串): ``` redis-cli SETEX key seconds value ``` 例如,设置key为session,过期时间3600秒,值为token123: ``` redis-cli SETEX session 3600 token123 ``` 另外,引用[4]中提到了String类型的一些命令,其中SET就是用来设置键值对的。 注意事项: 1. Redis对key名的大小写是敏感的。 2. 如果写入时出现错误,可能是由于认证失败(NOAUTH)或者数据库存在等问题,请检查连接参数。 根据上述内容,我们整理出写入数据的方法。 </think> ### 使用 redis-cli 向数据库写入键值对数据的方法 以下是详细的操作步骤示例,基于 Redis 官方文档最佳实践[^1][^3][^4]: #### 1. **基础写入:SET 命令** ```bash # 基本语法 SET key value # 示例:写入 name=Alice redis-cli SET name "Alice" ``` - 返回 `OK` 表示成功 - 验证写入:`redis-cli GET name` → 返回 `"Alice"` #### 2. **批量写入:MSET 命令** ```bash # 同时写入多个键值对 MSET key1 value1 key2 value2 ... # 示例:批量写入用户数据 redis-cli MSET user:1001:name "Bob" user:1001:age 30 user:1001:email "bob@example.com" ``` #### 3. **带过期时间的写入** ```bash # 设置键值对并指定过期时间(秒) SETEX key seconds value # 示例:写入 24 小时后过期的会话数据 redis-cli SETEX session_token 86400 "abcd1234efgh" ``` #### 4. **条件写入** ```bash # 仅当键存在时写入(防覆盖) SETNX key value # 示例:安全写入新配置 redis-cli SETNX config:max_users "1000" ``` #### 5. **指定数据库写入** ```bash # 方法1:启动时选择数据库-n 指定数据库索引) redis-cli -n 2 SET status "active" # 方法2:交互模式切换数据库 redis-cli 127.0.0.1:6379> SELECT 1 OK 127.0.0.1:6379[1]> SET log_level "debug" ``` #### 6. **带认证的远程写入** ```bash # 连接远程 Redis写入-h 主机 -p 端口 -a 密码) redis-cli -h 192.168.1.100 -p 6380 -a yourpassword SET server_status "online" ``` #### 7. **数据类型支持** - **数值类型**:自动识别数值操作 ```bash redis-cli SET counter 100 redis-cli INCR counter # → 101 ``` - **二进制数据**:支持任意二进制值 ```bash redis-cli SET image_data "$(cat photo.jpg | base64)" ``` #### 注意事项 1. **键名规范**:Redis 区分键名大小写(`Name` `name` 是两个同的键)[^1] 2. **值大小限制**:单个字符串值最大 512MB,避免写入过大值影响性能[^5] 3. **连接验证**:出现 `(error) NOAUTH` 时需检查密码 4. **持久化确认**:关键数据需检查持久化配置(`CONFIG GET save`) > **验证技巧**:写入后立即执行 `GET key` 或 `EXISTS key` 确认结果[^4]。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值