数据库缓存的一致性

来源 | OSCHINA 社区 作者 | 腾讯云开发者社区 原文链接:https://my.oschina.net/qcloudcommunity/blog/5596831 导语 缓存合理使用确提升了系统的吞吐量和稳定性,然而这是有代价的。这个代价便是缓存和数据库的一致性带来了挑战,本文将针对最常见的 cache-aside 策略下如何维护缓存一致性彻底讲透。 但是客观上,我们的业务规模很可能要求着更高的 QPS,有些业务的规模本身就非常大,也有些业务会遇到一些流量高峰,比如电商会遇到大促的情况。 而这时候大部分的流量实际上都是读请求,而且大部分数据也是没有那么多变化的,如热门商品信息、微博的内容等常见数据就是如此。此时,缓存就是我们应对此类场景的利器。 关于一致性的文章网上有很多,这篇整体条理比较清晰,从基本解决方案(四种),到多步操作中某一步失败可能导致的一系列问题及解决。如同结尾讲的那样,每一种都有其使用场景,应结合具体场景进行选择。 缓存的意义 所谓缓存,实际上就是用空间换时间,准确地说是用更高速的空间来换时间,从而整体上提升读的性能。 何为更高速的空间呢? 更快的存储介质。通常情况下,如果说数据库的速度慢,就得用更快的存储组件去替代它,目前最常见的就是 Redis(内存存储)。Redis 单实例的读 QPS 可以高达 10w/s,90% 的场景下只需要正确使用 Redis 就能应对。 就近使用本地内存。就像 CPU 也有高速缓存一样,缓存也可以分为一级缓存、二级缓存。即便 Redis 本身性能已经足够高了,但访问一次 Redis 毕竟也需要一次网络 IO,而使用本地内存无疑有更快的速度。不过单机的内存是十分有限的,所以这种一级缓存只能存储非常少量的数据,通常是最热点的那些 key 对应的数据。这就相当于额外消耗宝贵的服务内存去换取高速的读取性能。 引入缓存后的一致性挑战 用空间换时间,意味着数据同时存在于多个空间。最常见的场景就是数据同时存在于 Redis 与 MySQL 上(为了问题的普适性,后面举例中若没有特别说明,缓存均指 Redis 缓存)。 实际上,最权威最全的数据还是在 MySQL 里的。而万一 Redis 数据没有得到及时的更新(例如数据库更新了没更新到 Redis),就出现了数据不一致。 大部分情况下,只要使用了缓存,就必然会有不一致的情况出现,只是说这个不一致的时间窗口是否能做到足够的小。有些不合理的设计可能会导致数据持续不一致,这是我们需要改善设计去避免的。 这里的一致性实际上对于本地缓存也是同理的,例如数据库更新后没有及时更新本地缓存,也是有一致性问题的,下文统一以 Redis […]

Redia lua脚本,序列化问题

已知: lua脚本在redis中直接执行正常 redisTemplate执行lua脚本,出现各种错误,但用返回的sha,在redis中直接执行正常 初步判定,是redis的序列化的问题,出问题时,使用的RedisTemplate,且value的序列化使用的FastJsonRedisSerializer。推测在反序列化时,出现了问题 。 一个解决方案是,RedisTemplate用在通常的kv的操作中,与lua脚本有关的,使用StringRedisTemplate

从 0 到 1 构建一个稳定、高性能的 Redis 集群!(转)

CSDN Yesterday The following article is from 水滴与银弹 Author Magic Kaito 作者 | Magic Kaito 责编 | 张文 来源 | 水滴与银弹(ID:waterdrop_bullet) 这篇文章我想和你聊一聊 Redis 的架构演化之路。 现如今 Redis 变得越来越流行,几乎在很多项目中都要被用到,不知道你在使用 Redis 时,有没有思考过,Redis 到底是如何稳定、高性能地提供服务的? 你也可以尝试回答一下以下这些问题: 我使用 Redis 的场景很简单,只使用单机版 Redis 会有什么问题吗? 我的 Redis 故障宕机了,数据丢失了怎么办?如何能保证我的业务应用不受影响? 为什么需要主从集群?它有什么优势? 什么是分片集群?我真的需要分片集群吗? … 如果你对 Redis 已经有些了解,肯定也听说过数据持久化、主从复制、哨兵这些概念,它们之间又有什么区别和联系呢? 如果你存在这样的疑惑,这篇文章,我会从 0 到 1,再从 1 到 N,带你一步步构建出一个稳定、高性能的 Redis 集群。 在这个过程中,你可以了解到 […]

分布式缓存的十个坑,看你能挨住几个?

转自:Java技术迷 6 days ago “ 今天无聊来撩一下分布式缓存,希望你们喜欢~ 目录 前言 目前工作中用到的分布式缓存技术有redis和memcached两种,缓存的目的是为了在高并发系统中有效降低DB的压力,但是在使用的时候可能会因为缓存结构设计不当造成一些问题,这里会把可能遇到的坑整理出来,方便日后查找。 一. 常用的两种缓存技术的服务端特点 1. Memcache服务端 Memcache(下面简称mc)服务端是没有集群概念的,所有的存储分发全部交由mc client去做,我这里使用的是xmemcached,这个客户端支持多种哈希策略,默认使用key与实例数取模来进行简单的数据分片。 这种分片方式会导致一个问题,那就是新增或者减少节点后会在一瞬间导致大量key失效,最终导致缓存雪崩的发生,给DB带来巨大压力,所以我们的mc client启用了xmemcached的一致性哈希算法来进行数据分片: 根据一致性哈希算法的特性,在新增或减少mc的节点只会影响较少一部分的数据。但这种模式下也意味着分配不均匀,新增的节点可能并不能及时达到均摊数据的效果,不过mc采用了虚拟节点的方式来优化原始一致性哈希算法(由ketama算法控制实现),实现了新增物理节点后也可以均摊数据的能力。 最后,mc服务端是多线程处理模式,mc一个value最大只能存储1M的数据,所有的k-v过期后不会自动移除,而是下次访问时与当前时间做对比,过期时间小于当前时间则删除,如果一个k-v产生后就没有再次访问了,那么数据将会一直存在在内存中,直到触发LRU。 2. Redis服务端 redis服务端有集群模式,key的路由交由redis服务端做处理,除此之外redis有主从配置以达到服务高可用。 redis服务端是单线程处理模式,这意味着如果有一个指令导致redis处理过慢,会阻塞其他指令的响应,所以redis禁止在生产环境使用重量级操作(例如keys,再例如缓存较大的值导致传输过慢) redis服务端并没有采用一致性哈希来做数据分片,而是采用了哈希槽的概念来做数据分片,一个redis cluster整体拥有16384个哈希槽(slot),这些哈希槽按照编号区间的不同,分布在不同节点上,然后一个key进来,通过内部哈希算法(CRC16(key))计算出槽位置; 然后将数据存放进对应的哈希槽对应的空间,redis在新增或者减少节点时,其实就是对这些哈希槽进行重新分配,以新增节点为例,新增节点意味着原先节点上的哈希槽区间会相对缩小,被减去的那些哈希槽里的数据将会顺延至下一个对应节点,这个过程由redis服务端协调完成,过程如下: 图1 “ 迁移过程是以槽为单位,将槽内的key按批次进行迁移的(migrate)。 二. 缓存结构化选型 mc提供简单的k-v存储,value最大可以存储1M的数据,多线程处理模式,不会出现因为某次处理慢而导致其他请求排队等待的情况,适合存储数据的文本信息。 redis提供丰富的数据结构,服务端是单线程处理模式,虽然处理速度很快,但是如果有一次查询出现瓶颈,那么后续的操作将被阻塞,所以相比k-v这种可能因为数据过大而导致网络交互产生瓶颈的结构来说,它更适合处理一些数据结构的查询、排序、分页等操作,这些操作往往复杂度不高,且耗时极短,因此不太可能会阻塞redis的处理。 使用这两种缓存服务来构建我们的缓存数据,目前提倡所有数据按照标志性字段(例如id)组成自己的信息缓存存储,这个一般由mc的k-v结构来完成存储。 而redis提供了很多好用的数据结构,一般构建结构化的缓存数据都使用redis的数据结构来保存数据的基本结构,然后组装数据时根据redis里缓存的标志性字段去mc里查询具体数据,例如一个排行榜接口的获取: 图2 上图redis提供排行榜的结构存储,排行榜里存储的是id和score,通过redis可以获取到结构内所有信息的id,然后利用获得的id可以从mc中查出详细信息,redis在这个过程负责分页、排序,mc则负责存储详细信息。 上面是比较合适的缓存做法,建议每条数据都有一个自己的基本缓存数据,这样便于管理,而不是把一个接口的巨大结构完全缓存到mc或者redis里,这样划分太粗,日积月累下来每个接口或者巨大方法都有一个缓存,key会越来越多,越来越杂。 三. Redis构造大索引回源问题 Redis如果做缓存使用,始终会有过期时间存在,如果到了过期时间,使用redis构建的索引将会消失,这个时候回源,如果存在大批量的数据需要构建redis索引,就会存在回源方法过慢的问题,这里以某个评论系统为例; 评论系统采用有序集合作为评论列表的索引,存储的是评论id,用于排序的score值则按照排序维度拆分,比如发布时间、点赞数等,这也意味着一个资源下的评论列表根据排序维度不同存在着多个redis索引列表,而具体评论内容存mc,正常情况下结构如下: 图3 上面是正常触发一个资源的评论区,每次触发读缓存,都会顺带延长一次缓存的过期时间,这样可以保证较热的内容不会轻易过期,但是如果一个评论区时间过长没人访问过,redis索引就会过期,如果一个评论区有数万条评论数据,长时间没人访问,突然有人过去考古,那么在回源构建redis索引时会很缓慢,如果没有控制措施,还会造成下面缓存穿透的问题,从而导致这种重量级操作反复被多个线程执行,对DB造成巨大压力。 对于上面这种回源构建索引缓慢的问题,处理方式可以是下面这样: 图4 相比直接执行回源方法,这种通过消息队列构造redis索引的方法更加适合,首先仅构建单页或者前面几页的索引数据,然后通过队列通知job(这里可以理解为消费者)进行完整索引构造,当然,这只适合对一致性要求不高的场景。 四. 一致性问题 一般情况下缓存内的数据要和数据库源数据保持一致性,这就涉及到更新DB后主动失效缓存策略(通俗叫法:清缓存),大部分会经过如下过程: 图5 假如现在有两个服务,服务A和服务B,现在假设服务A会触发某个数据的写操作,而服务B则是只读程序,数据被缓存在一个Cache服务内,现在假如服务A更新了一次数据库,那么结合上图得出以下流程: 服务A触发更新数据库的操作 更新完成后删除数据对应的缓存key 只读服务(服务B)读取缓存时发现缓存miss […]

使用Redis实现内容过期提醒

# 业务场景 我们以订单功能为例说明下:生成订单后一段时间不支付订单会自动关闭。最简单的想法是设置定时任务轮询,但是每个订单的创建时间不一样,定时任务的规则无法设定,如果将定时任务执行的间隔设置的过短,太影响效率。还有一种想法,在用户进入订单界面的时候,判断时间执行相关操作。方式可能有很多,在这里介绍一种监听 Redis 键值对过期时间来实现订单自动关闭。 # 实现思路 在生成订单时,向 Redis 中增加一个 KV 键值对,K 为订单号,保证通过 K 能定位到数据库中的某个订单即可,V 可为任意值。假设,生成订单时向 Redis 中存放 K 为订单号,V 也为订单号的键值对,并设置过期时间为 30 分钟,如果该键值对在 30 分钟过期后能够发送给程序一个通知,或者执行一个方法,那么即可解决订单关闭问题。实现:通过监听 Redis 提供的过期队列来实现,监听过期队列后,如果 Redis 中某一个 KV 键值对过期了,那么将向监听者发送消息,监听者可以获取到该键值对的 K,注意,是获取不到 V 的,因为已经过期了,这就是上面所提到的,为什么要保证能通过 K 来定位到订单,而 V 为任意值即可。拿到 K 后,通过 K 定位订单,并判断其状态,如果是未支付,更新为关闭,或者取消状态即可。 # 开启 Redis key 过期提醒 修改 redis 相关事件配置。找到 redis 配置文件 redis.conf,查看 notify-keyspace-events 配置项,如果没有,添加 […]

缓存注解的使用

一、Cache缓存简介 从Spring3开始定义Cache和CacheManager接口来统一不同的缓存技术; Cache接口为缓存的组件规范定义,包含缓存的各种操作集合; Cache接口下Spring提供了各种缓存的实现; 如RedisCache,EhCacheCache ,ConcurrentMapCache等; 二、核心API 1、Cache缓存接口 定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 2、CacheManager 缓存管理器,管理各种缓存(cache)组件 3、@Cacheable 主要针对方法配置,能够根据方法的请求参数对其进行缓存 Cacheable 执行流程1)方法运行之前,按照cacheNames指定的名字先去查询Cache 缓存组件2)第一次获取缓存如果没有Cache组件会自动创建3)Cache中查找缓存的内容,使用一个key,默认就是方法的参数4)key是按照某种策略生成的;默认是使用keyGenerator生成的,这里使用自定义配置5)没有查到缓存就调用目标方法;6)将目标方法返回的结果,放进缓存中​Cacheable 注解属性cacheNames/value:指定方法返回结果使用的缓存组件的名字,可以指定多个缓存key:缓存数据使用的keykey/keyGenerator:key的生成器,可以自定义cacheManager:指定缓存管理器cacheResolver:指定缓存解析器condition:指定符合条件的数据才缓存unless:否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存sync:是否使用异步模式 4、@CacheEvict 清除缓存 CacheEvict:缓存清除key:指定要清除的数据allEntries = true:指定清除这个缓存中所有的数据beforeInvocation = false:方法之前执行清除缓存,出现异常不执行beforeInvocation = true:代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除 5、@CachePut 保证方法被调用,又希望结果被缓存。 与@Cacheable区别在于是否每次都调用方法,常用于更新,写入 CachePut:执行方法且缓存方法执行的结果修改了数据库的某个数据,同时更新缓存;执行流程 1)先调用目标方法 2)然后将目标方法的结果缓存起来 6、@EnableCaching 开启基于注解的缓存 7、keyGenerator 缓存数据时key生成策略 8、@CacheConfig 统一配置本类的缓存注解的属性 三、与SpringBoot2.0整合 1、核心依赖 <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-cache</artifactId></dependency><dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-data-redis</artifactId></dependency> 2、Cache缓存配置 import org.springframework.cache.interceptor.KeyGenerator;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.lang.reflect.Method;@Configurationpublic […]

lWoHvYe 无悔,专一