什么情况下会出现数据库和缓存不一致的问题?

典型回答 首先,在非并发的场景中,出现不一致的问题大家都能比较容易的理解,因为缓存的操作和数据库的操作是存在一定的时间差的。而且这两个操作是没办法保证原子性的,也就是说,是有可能一个操作成功,一个操作失败的。所以,这就必然会存在不一致的情况。 如果在并发场景中,如果两个线程,同时进行先写数据库,后更新缓存的操作,就可能会出现不一致: W W 写数据库,更新成20 写数据库,更新成10 写缓存,更新成10 写缓存,更新成20(数据不一致) 如果在并发场景中,如果两个线程,同时进行先更新缓存,后写数据库的操作,同理,也可能会出现不一致: W W 写缓存,更新成20 写缓存,更新成10 写数据库,更新成10 写数据库,更新成20(数据不一致) 在并发场景中,还有一种容易忽略的并发场景,那就是读写并发。 我们知道,当我们使用了缓存之后,一个读的线程在查询数据的过程是这样的: 1、查询缓存,如果缓存中有值,则直接返回 2、查询数据库 3、把数据库的查询结果更新到缓存中 所以,对于一个读线程来说,虽然不会写数据库,但是是会更新缓存的,所以,在一些特殊的并发场景中,就会导致数据不一致的情况。 读写并发的时序如下: W R 读缓存,缓存中没有值 读数据库,数据库中得到结果为10 写数据库和缓存,更新成20 写缓存,更新成10(数据不一致) 也就是说,假如一个读线程,在读缓存的时候没查到值,他就会去数据库中查询,但是如果自查询到结果之后,更新缓存之前,数据库被更新了,但是这个读线程是完全不知道的,那么就导致最终缓存会被重新用一个”旧值”覆盖掉。 ...

March 22, 2026 · 1 min · santu

Redis如何实现延迟消息?

Redis过期消息实现延迟消息 很多用过Redis的人都知道,Redis有一个过期监听的功能, 在 redis.conf 中,加入一条配置notify-keyspace-events Ex开启过期监听,然后在代码中实现一个KeyExpirationEventMessageListener,就可以监听key的过期消息了。 这样就可以在接收到过期消息的时候,进行订单的关单操作。 这个方案不建议大家使用,是因为Redis官网上明确的说过,Redis并不保证Key在过期的时候就能被立即删除,更不保证这个消息能被立即发出。所以,消息延迟是必然存在的,随着数据量越大延迟越长,延迟个几分钟都是常事儿。 而且,在Redis 5.0之前,这个消息是通过PUB/SUB模式发出的,他不会做持久化,至于你有没有接到,有没有消费成功,他不管。也就是说,如果发消息的时候,你的客户端挂了,之后再恢复的话,这个消息你就彻底丢失了。 Redis的zset实现延迟消息 虽然基于Redis过期监听的方案并不完美,但是并不是Redis实现关单功能就不完美了,还有其他的方案。 我们可以借助Redis中的有序集合——zset来实现这个功能。 zset是一个有序集合,每一个元素(member)都关联了一个 score,可以通过 score 排序来取集合中的值。 我们将订单超时时间的时间戳(下单时间+超时时长)与订单号分别设置为 score 和 member。这样redis会对zset按照score延时时间进行排序。然后我们再开启redis扫描任务,获取”当前时间 > score”的延时任务,扫描到之后取出订单号,然后查询到订单进行关单操作即可。 使用redis zset来实现订单关闭的功能的优点是可以借助redis的持久化、高可用机制。避免数据丢失。但是这个方案也有缺点,那就是在高并发场景中,有可能有多个消费者同时获取到同一个订单号,一般采用加分布式锁解决,但是这样做也会降低吞吐型。 但是,在大多数业务场景下,如果幂等性做得好的,多个消费者取到同一个订单号也无妨。 Redission实现延迟消息 上面这种方案看上去还不错,但是需要我们自己基于zset这种数据结构编写代码,那么有没有什么更加友好的方式? 有的,那就是基于Redisson。 Redisson是一个在Redis的基础上实现的框架,它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。 Redission中定义了分布式延迟队列RDelayedQueue,这是一种基于我们前面介绍过的zset结构实现的延时队列,它允许以指定的延迟时长将元素放到目标队列中。 其实就是在zset的基础上增加了一个基于内存的延迟队列。当我们要添加一个数据到延迟队列的时候,redission会把数据+超时时间放到zset中,并且起一个延时任务,当任务到期的时候,再去zset中把数据取出来,返回给客户端使用。 大致思路就是这样的,感兴趣的大家可以看一看RDelayedQueue的具体实现。 基于Redisson的实现方式,是可以解决基于zset方案中的并发重复问题的,而且还能实现方式也比较简单,稳定性、性能都比较高。

March 22, 2026 · 1 min · santu

如何用SETNX实现分布式锁?

典型回答 利用Redis的单线程特性,在多个Redis客户端同时通过SETNX命令尝试获取锁,如果返回1表示获取锁成功,否则表示获取锁失败。 Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1 。 设置失败,返回 0 。 因为Redis的单线程机制,所以可以保证只会有一个客户端成功获取到锁,而其他客户端则会失败。如果获取锁成功,则设置一个过期时间,防止该客户端挂了之后一直持有该锁。客户端释放锁的时候,需要先判断该锁是否仍然属于该客户端,如果是,则通过DEL命令释放锁。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class RedisDistributedLock { private final JedisPool jedisPool; public RedisDistributedLock(JedisPool jedisPool) { this.jedisPool = jedisPool; } public boolean tryLock(String lockKey, String requestId, int expireTime) { try (Jedis jedis = jedisPool.getResource()) { String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); return "OK".equals(result); } } public boolean unlock(String lockKey, String requestId) { try (Jedis jedis = jedisPool.getResource()) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); return Long.parseLong(result.toString()) == 1L; } } } tryLock方法接收三个参数,分别是锁的键值lockKey、加锁的请求标识requestId和锁的过期时间expireTime。该方法会尝试使用Redis的set命令加锁,如果加锁成功则返回true,否则返回false。其中NX表示只在锁的键不存在时设置锁,PX表示锁的过期时间为expireTime毫秒。 ...

March 22, 2026 · 1 min · santu

对于 Redis 的操作,有哪些推荐的 Best Practices?

典型回答 避免使用 KEYS 命令获取所有 key,因为该命令会遍历所有 key,可能会阻塞 Redis 的主线程。 避免使用 FLUSHALL 或 FLUSHDB 命令清空 Redis 数据库,因为这会清空所有数据库中的数据,而不仅仅是当前数据库。 避免在 Redis 中存储大的数据块,因为这会导致 Redis 实例内存占用过高,影响 Redis 的性能。 合理设置过期时间,避免过期时间设置过短或过长,导致 Redis 实例内存占用过高或数据过期失效时间不准确。 对于写入操作频繁的数据,考虑使用 Redis 的持久化机制进行数据持久化,以保证数据的可靠性。 避免使用 Lua 脚本中的无限循环,因为这会导致 Redis 的主线程被阻塞。 对于需要频繁更新的数据,可以使用 Redis 的 Hash 数据结构,以减少 Redis 实例的内存占用和网络传输数据量。因为Hash可以做部分更新。 避免在 Redis 实例上运行复杂的计算逻辑,因为这会导致 Redis 的主线程被阻塞,影响 Redis 的性能。 对于需要高可用的 Redis 实例,可以使用 Redis Sentinel 或 Redis Cluster 进行搭建,以实现 Redis 的高可用性。 对于需要高并发的场景,可以使用 Redis 的分布式锁机制,以避免并发访问数据的冲突。

March 22, 2026 · 1 min · santu

除了做缓存,Redis还能用来干什么?

典型回答 Redis最主要的功能就是拿来做缓存,来提升系统的性能,但是除了做缓存以外,他还能做很多事(但是,能做并不代表就适合,并不代表就一定要用它): 消息队列(不建议):Redis 支持发布/订阅模式和Stream,可以作为轻量级消息队列使用,用于异步处理任务或处理高并发请求。 延迟消息(不建议):Redis的ZSET可以用来实现延迟消息,也可以基于Key的过期消息实现延迟消息,还可以借助Redisson的RDelayQueue来实现延迟消息,都是可以的。 排行榜(建议):利用Redis 的有序集合和列表结构,可以成为设计实时排行榜的绝佳选择,例如各类热门排行榜、热门商品列表等。 计数器(建议):基于Redis可以实现一些计数器的功能,比如网站的访问量、朋友圈点赞等。通过 incr 命令就能实现原子性的自增操作,从而实现一个全局计数器。· 分布式ID(可以):因为他有全局自增计数的功能,所以在分布式场景,我们也可以利用Redis来实现一个分布式ID来保障全局的唯一且自增。 分布式锁(建议):Redis 的单线程特性可以保证多个客户端之间对同一把锁的操作是原子性的,可以轻松实现分布式锁,用于控制多个进程对共享资源的访问。 地理位置应用(建议):Redis 支持GEO,支持地理位置定位和查询,可以存储地理位置信息并通过 Redis 的查询功能获取附近的位置信息。比如"附近的人"用它来实现就非常方便。 分布式限流(可以):Redis提供了令牌桶和漏桶算法的实现,可以用于实现分布式限流。 分布式Session(建议):可以使用Redis实现分布式Session管理,保证多台服务器之间用户的会话状态同步。 布隆过滤器(建议):Redis提供了布隆过滤器(Bloom Filter)数据结构的实现,可以高效地检测一个元素是否存在于一个集合中 状态统计(数据量大建议用):Redis中支持BitMap这种数据结构,它不仅查询和存储高效,更能节省很多空间,所以我们可以借助他做状态统计,比如记录亿级用户的登录状态,或者是拿他来做签到统计也比较常见。 **共同关注(建议):**Redis中支持Set集合类型,这个类型非常适合我们做一些取并集、交集、差集等,基于这个特性,我们就能取交集的方式非常方便的实现共同好友、或者共同关注的功能。 推荐关注(可以):和上面的共同关注类似,交集实现共同好友,那么并集或者差集就能实现推荐关注的功能。

March 22, 2026 · 1 min · santu

为什么ZSet 既能支持高效的范围查询,还能以 O(1) 复杂度获取元素权重值?

典型回答 Sorted Set 能支持范围查询,这是因为它的核心数据结构设计采用了跳表,而它又能O(1)的复杂度获取元素权重,这是因为它同时采用了哈希表进行索引。 1 2 3 4 5 typedef struct zset { dict *dict; zskiplist *zsl; } zset; 以上是zset的数据结构,其中包含了两个成员,分别是哈希表dict和跳表zsl。 dict存储 member->score 之间的映射关系,所以 ZSCORE 的时间复杂度为 O(1)。skiplist 是一个「有序链表 + 多层索引」的结构,查询元素的复杂度是 O(logN),所以他的查询效率很高。

March 22, 2026 · 1 min · santu

如何用Redisson实现分布式锁?

典型回答 ✅如何用SETNX实现分布式锁? 在使用SETNX实现的分布式锁中,因为存在锁无法续期导致并发冲突的问题,所以在真实的生产环境中用的并不是很多,其实,真正在使用Redis时,用的比较多的是基于Redisson实现分布式锁。 Redisson是一个基于Redis的Java客户端,它提供了丰富的功能,包括分布式锁的支持。 https://redisson.org/ 关于Redisson实现分布式锁可以查看:https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers 为了避免锁超时,Redisson中引入了看门狗的机制,他可以帮助我们在Redisson实例被关闭前,不断的延长锁的有效期。 默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。 可重入锁 基于Redisson可以非常简单的就获取一个可重入的分布式锁。基本步骤如下: 引入依赖: 1 2 3 4 5 <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>最新版</version> </dependency> 定义一个Redisson客户端: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * @author Hollis */ @Configuration public class RedissonConfig { @Bean(destroyMethod="shutdown") public RedissonClient redisson() throws IOException { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); return redisson; } } 接下来,在想要使用分布式锁的地方做如下调用即可: ...

March 22, 2026 · 1 min · santu

如何基于Redisson实现一个延迟队列

典型回答 Redisson中定义了分布式延迟队列RDelayedQueue,这是一种基于我们前面介绍过的zset结构实现的延时队列,它允许以指定的延迟时长将元素放到目标队列中。 其实就是在zset的基础上增加了一个基于内存的延迟队列。当我们要添加一个数据到延迟队列的时候,redisson会把数据+超时时间放到zset中,并且起一个延时任务,当任务到期的时候,再去zset中把数据取出来,返回给客户端使用。 1 2 3 4 5 <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>最新版</version> </dependency> 定义一个Redisson客户端: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * @author Hollis */ @Configuration public class RedissonConfig { @Bean(destroyMethod="shutdown") public RedissonClient redisson() throws IOException { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); return redisson; } } 接下来,在想要使用延迟队列的地方做如下方式: ...

March 22, 2026 · 1 min · santu

Redis中有一批key瞬间过期,为什么其它key的读写效率会降低?

典型回答 ✅Redis中key过期了一定会立即删除吗 Redis的键有两种过期方式:一种是被动过期,另一种是主动过期。 主动过期会在定时的去删除key,那么带来一个问题,那就是:Redis中如果有一批key同时过期,会导致其它key的读写效率降低 原因是因为Redis的主动过期定时任务也是在Redis的单线程模型中的主线程中执行的,也就是说如果出现了一批key同时过期,就需要删除大量的Key。那么因为命令执行是单线程的,所以这时候后面来的业务操作请求,就需要等这个删除命令执行完才可以处理业务请求。 那么这时候就会出现,业务访问延时增大的问题。 **如何解决这个问题呢?**有以下几个思路: 使用随机过期时间:如果可能的话,可以将键的过期时间随机分布,而不是在同一时间点过期。这样可以分散过期键删除的压力,避免大规模的键同时过期。同时也能避免缓存雪崩的问题。 使用被动删除:被动删除在每次访问的时候才会去删除key,并不会定时删除,不太容易发生大量key同时被删除的情况。但是被动删除可能会导致内存占用比较多。

March 22, 2026 · 1 min · santu

Redisson和Jedis有啥区别?如何选择?

典型回答 Redisson和Jedis是两个流行的Java客户端库,用于与Redis进行交互,其实在Redisson的官网上针对这两个产品做了比较全面的对比:https://redisson.org/feature-comparison-redisson-vs-jedis.html 一句话就是Jedis非常的轻量级,极其简单,可以认为就是把Redis的命令做了一下封装,而Redisson提供了更多高级特性和功能,整体也更加复杂一些。 以下是我基于官网上的资料做的简单整理: 分布式集合: Redisson:提供多种Java集合对象的实现,包括Multimap、PriorityQueue、DelayedQueue等 Jedis:支持较少的分布式集合,大多只支持Map、Set、List等的基本命令。 分布式锁和同步器: Redisson:支持常见的Java锁和同步器,如FairLock、MultiLock、Semaphore、CountDownLatch等。 Jedis:不支持。需要自己实现 分布式对象: Redisson:实现了多种分布式对象,如Publish/Subscribe、BloomFilter、RateLimiter、Id Generator等。 Jedis:只支持基本的类型的基本命令,如AtomicLong、AtomicDouble、HyperLogLog等 高级缓存支持: Redisson:提供多种高级缓存功能,支持Read-through/Write-through/Write-behind等策略。 Jedis:不支持这些高级缓存功能。 API架构: Redisson:支持实例线程安全、异步接口、响应式流接口和RxJava3接口。 Jedis:不支持。 分布式服务: Redisson:提供ExecutorService、MapReduce、SchedulerService等服务。 Jedis:不支持这些分布式服务。 框架集成: Redisson:支持Spring Cache、Hibernate Cache、MyBatis Cache等。 Jedis:仅支持Spring Session和Spring Cache。 安全性: Redisson和Jedis:都支持认证和SSL。 自定义数据序列化: Redisson:支持多种编解码器,如JSON、JDK序列化、Avro等。 Jedis:不支持JDK序列化或上述编解码器。 所以我们在选择的时候,如果需要高级特性如分布式锁、高级缓存支持或特定框架集成,Redisson可能是更好的选择。如果项目需要一个轻量级的解决方案,且不需要高级功能,Jedis可能是合适的选择。

March 22, 2026 · 1 min · santu

留言给博主