血的教训!千万别在生产使用这些-redis-指令了!

本文探讨了Rediskeys指令导致命令执行变慢的原因,主要在于其全量扫描底层哈希表的高时间复杂度。相比之下,Scan命令通过分页和游标机制避免了全量扫描,提供了解决方案。作者还强调了在生产环境中应避免阻塞性操作,提倡使用Scan等渐进式查询方法。

}finally{
jedis.close();
}
return keys;

}

虽然问题成功解决了,但是小黑哥心里还是有点不解。

为什么 keys 指令会导致其他命令执行变慢?

为什么 Keys 指令查询会这么慢?

为什么 Scan 指令就没有问题?

Redis 执行命令的原理

首先我们来看第一个问题,为什么 keys 指令会导致其他命令执行变慢?

回答这个问题,我们首先看下 Redis 客户端执行一条命令的情况:

站在客户端的视角,执行一条命令分为三步:

  1. 发送命令
  2. 执行命令
  3. 返回结果

但是这仅仅客户端自己以为的过程,但是实际上同一时刻,可能存在很多客户端发送命令给 Redis,而 Redis 我们都知道它采用的是单线程模型。

为了处理同一时刻所有的客户端的请求命令,Redis 内部采用了队列的方式,排队执行。

于是客户端执行一条命令实际需要四步:

  1. 发送命令
  2. 命令排队
  3. 执行命令
  4. 返回结果

由于 Redis 单线程执行命令,只能顺序从队列取出任务开始执行。

只要 3 这个过程执行命令速度过慢,队列其他任务不得不进行等待,这对外部客户端看来,Redis 好像就被阻塞一样,一直得不到响应。

所以使用 Redis 过程切勿执行需要长时间运行的指令,这样可能导致 Redis 阻塞,影响执行其他指令。

KEYS 原理

接下来开始回答第二个问题,为什么 Keys 指令查询会这么慢?

回答这个问题之前,请大家回想一下 Redis 底层存储结构。

这里小黑哥复制之前文章内容,Redis 底层使用字典这种结构,这个结构与 Java HashMap 底层比较类似。

keys命令需要返回所有的符合给定模式 pattern 的 Redis 中键,为了实现这个目的,Redis 不得不遍历字典中 ht[0]哈希表底层数组,这个时间复杂度为 O(N)(N 为 Redis 中 key 所有的数量)。

如果 Redis 中 key 的数量很少,那么这个执行速度还是也会很快。等到 Redis key 的数量慢慢更加,上升到百万、千万、甚至上亿级别,那这个执行速度就会很慢很慢。

下面是小黑哥本地做的一次实验,使用 lua 脚本往 Redis 中增加 10W 个 key,然后使用 keys 查询所有键,这个查询大概会阻塞十几秒的时间。

eval “for i=1,100000 do redis.call(‘set’,i,i+1) end” 0

这里小黑哥使用 Docker 部署 Redis,性能可能会稍差。

SCAN 原理

最后我们来看下第三个问题,为什么 scan 指令就没有问题?

这是因为 scan命令采用一种黑科技-基于游标的迭代器

每次调用 scan 命令,Redis 都会向用户返回一个新的游标以及一定数量的 key。下次再想继续获取剩余的 key,需要将这个游标传入 scan 命令, 以此来延续之前的迭代过程。

简单来讲,scan 命令使用分页查询 redis 。

下面是一个 scan 命令的迭代过程示例:

scan 命令使用游标这种方式,巧妙将一次全量查询拆分成多次,降低查询复杂度。

虽然 scan 命令时间复杂度与 keys一样,都是 O(N),但是由于 scan 命令只需要返回少量的 key,所以执行速度会很快。

最后,虽然scan 命令解决 keys不足,但是同时也引入其他一些缺陷:

  • 同一个元素可能会被返回多次,这就需要我们应用程序增加处理重复元素功能。
  • 如果一个元素在迭代过程增加到 redis,或者说在迭代过程被删除,那个这个元素会被返回,也可能不会。

以上这些缺陷,在我们开发中需要考虑这种情况。

除了 scan以外,redis 还有其他几个用于增量迭代命令:

  • sscan:用于迭代当前数据库中的数据库键,用于解决 smembers 可能产生阻塞问题
  • hscan命令用于迭代哈希键中的键值对,用于解决 hgetall 可能产生阻塞问题。
  • zscan:命令用于迭代有序集合中的元素(包括元素成员和元素分值),用于产生 zrange 可能产生阻塞问题。

总结

Redis 使用单线程执行操作命令,所有客户端发送过来命令,Redis 都会现放入队列,然后从队列中顺序取出执行相应的命令。

如果任一任务执行过慢,就会影响队列中其他任务的,这样在外部客户端看来,迟迟拿不到 Redis 的响应,看起来就很阻塞了一样。

所以不要在生产执行 keyssmembershgetallzrange这类可能造成阻塞的指令,如果真需要执行,可以使用相应的scan 命令渐进式遍历,可以有效防止阻塞问题。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

最后,强调几点:

  • 1. 一定要谨慎对待写在简历上的东西,一定要对简历上的东西非常熟悉。因为一般情况下,面试官都是会根据你的简历来问的; 能有一个上得了台面的项目也非常重要,这很可能是面试官会大量发问的地方,所以在面试之前好好回顾一下自己所做的项目;
  • 2. 和面试官聊基础知识比如设计模式的使用、多线程的使用等等,可以结合具体的项目场景或者是自己在平时是如何使用的;
  • 3. 注意自己开源的Github项目,面试官可能会挖你的Github项目提问;

我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!

以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目。

面试答案

三面头条+四面阿里+五面腾讯拿offer分享面经总结,最终入职阿里

三面头条+四面阿里+五面腾讯拿offer分享面经总结,最终入职阿里

三面头条+四面阿里+五面腾讯拿offer分享面经总结,最终入职阿里

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
mg-gT4MpYL1-1712990203327)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

<think>首先,用户在问的是在SpringBoot项目开发中使用synchronized的关键场景和最佳实践。用户引用了几个站内引用,我需要参考这些引用来提供回答。从引用中提取相关信息:-引用[1]:讨论了SpringBoot在高并发场景下的性能提升,提到了缓存组件EhCache和Redis,Tomcat线程池配置,服务器硬件资源,负载均衡,高可用部署。但没有直接提到synchronized。-引用[2]:关于监听器在SpringBoot中的使用,提到监听Servlet上下文对象用于初始化数据和缓存。没有提到synchronized。-引用[3]:涉及使用Netty开发WebSocket服务在SpringBoot中,提到了多线程。这可能与synchronized相关,但具体未讨论使用场景。-引用[4]:直接讨论了Spring事务管理下synchronized锁失效问题,提到在@Transactional方法中使用synchronized可能导致数据库更新不一致,给出了一个例子:@TransactionalpublicsynchronizedStringgenerateNewCode(),在高并发下转账金额不对。所以,引用[4]是最相关的,它强调在SpringBoot中使用synchronized时可能遇到的问题,特别是在事务管理上下文中的锁失效。用户的问题:synchronized在SpringBoot开发中的使用场景及最佳实践。系统指令-所有行内数学表达式必须使用$...$格式,但用户问题中没有数学内容,所以可能不需要。-独立公式必须使用$$...$$格式并单独成段,如果涉及数学时。-LaTeX语法正确。-使用中文回答。-生成相关问题。-回答中引用的段落末尾自然地添加引用标识,例如[^1]。-回答结构清晰,帮助用户逐步解决问题,保证真实可靠。我的回答结构:1.**介绍**:简述synchronized的作用和它在SpringBoot环境中的重要性。2.**使用场景**:列出在SpringBoot中常见的使用synchronized的关键场景。3.**最佳实践**:讨论如何正确使用synchronized,避免常见问题。4.**注意事项和潜在问题**:基于引用[4],解释在Spring事务中使用synchronized可能导致的问题。5.**解决方案和替代方案**:建议最佳实践和可选技术。6.**结尾**:生成相关问题。7.**引用标识**:自然地添加引用到相关部分。关键使用场景(在SpringBoot上下文中):-**共享资源访问**:例如,当多个线程需要访问共享数据时,如全局缓存、单例Bean的属性。-**代码块同步**:在并发操作中,确保原子性操作,如生成唯一ID、计数器。-**防止竞态条件**:在服务方法中避免数据不一致。-**基于引用[4]的事务问题**:在Spring的事务管理下,使用synchronized可能不足以保证原子性,因为事务提交发生在方法调用后。最佳实践:1.**避免在事务方法中使用synchronized**:如引用[4]所示,synchronized只在同一个JVM中有效,Spring事务涉及数据库,可能导致锁失效。最佳实践是使用数据库级别的锁或乐观锁。2.**最小化锁定范围**:使用synchronized块而不是整个方法,锁定共享资源。3.**利用Spring的并发工具**:使用@Scope("prototype")为Bean创建多实例,或使用java.util.concurrent包中的工具如ReentrantLock。4.**使用synchronizedonvolatile字段**:如果必须使用,确保字段volatile以避免可见性问题。5.**在无状态服务中谨慎使用**:如果服务Bean是无状态的,可能不需要synchronized。6.**测试和监控**:在高并发场景下测试,使用工具监控线程行为。从引用[4],具体的教训-@Transactional和synchronized结合时,事务提交在方法执行后,不同线程可能同时执行方法,导致数据不一致。-最佳实践是使用数据库锁如SELECTFORUPDATE或乐观锁如@VersioninJPA。回答时用中文。生成相关问题:如系统指令所示,类似于示例中的"相关问题"部分。生成3-5个相关的问题。结构:-**引入问题**-**使用场景**-**最佳实践**-**注意事项**-**解决方案**-**相关问题**添加引用:当提到引用[4]时,在段落末尾加[^4]。现在,草拟回答大纲:1.**介绍**:SpringBoot项目开发中,synchronized用于实现线程同步,但需注意在事务环境和分布式系统中的限制。2.**关键使用场景**:-共享资源访问,如全局变量或Bean属性。-原子操作,如生成唯一序列号。-引用监听器或WebSocket处理。3.**最佳实践**:-优先使用数据库锁或Spring的并发机制。-如果使用synchronized,确保在单一JVM中,并使用最小范围。-避免在@Transactional方法中结合synchronized(参考[^4])。4.**潜在问题与解决方案**:-问题:事务中的锁失效(如引用[^4])。-解决方案:使用@Lock注解、乐观锁、或外部分布式锁。5.**结论和建议**。6.**相关问题**。在数学表达式中:如果没有数学,不需添加。但如果提及计数器或性能分析,可能用到。暂时假设不涉及复杂数学。最终回答用中文。</think> ### Spring Boot 开发中 synchronized 的使用场景及最佳实践 在 Spring Boot 项目开发中,`synchronized` 关键字用于实现线程同步,确保共享资源在多线程环境下的安全访问,尤其在高并发场景下处理竞态条件(race conditions)时尤为关键。然而,Spring Boot 的依赖注入和事务管理机制(如 `@Transactional`)可能引入额外复杂性,导致 `synchronized` 失效或性能下降(如引用[4]所示:在高并发转账操作中,`@Transactional` 与 `synchronized` 结合使用时,数据库数据出现不一致)。下面我将逐步分析关键使用场景、最佳实践、注意事项以及解决方案,帮助你安全高效地使用 `synchronized`。本回答基于 Spring Boot 的标准机制和常见并发模式。 --- ### 一、synchronized 在 Spring Boot 中的关键使用场景 在 Spring Boot 环境中,`synchronized` 主要用于保护共享资源的原子性和一致性,尤其是在单实例 JVM(单一应用部署)场景下。以下是常见的应用场景: 1. **共享 Bean 属性同步**: - 当多个线程访问 Spring Bean 的共享状态(如全局计数器、缓存变量)时,需使用 `synchronized` 确保原子操作。 - 示例:一个单例(Singleton)Bean 的计数器属性,用于统计 API 调用次数。 ```java @Service public class CounterService { private int count = 0; // 共享属性 // 使用 synchronized 方法确保递增操作的原子性 public synchronized void increment() { count++; } } ``` - 在这种场景下,`synchronized` 能防止多个线程同时操作导致的数据不一致[^4]。 2. **生成唯一 ID 或序列号**: - 当业务需要生成全局唯一序列(如订单号、UUID)时,`synchronized` 可确保操作序列化。 - 示例:一个服务类中的方法,生成不重复的代码(如引用[4]中的 `generateNewCode()` 方法)。 ```java @Service public class CodeGenerator { // 生成新代码,使用 synchronized 块锁定临界区 public synchronized String generateNewCode() { // 复杂生成逻辑,确保每次返回唯一值 return "code-" + System.currentTimeMillis(); } } ``` - 这在无外部分布式锁的情况下,适用于低并发需求。 3. **监控与监听器初始化**: - 在 Spring Boot 的监听器(如 Servlet 上下文监听器)中,初始化和更新共享缓存时,需同步操作以数据安全(如引用[2]中提到的缓存初始化)。 ```java @WebListener public class CacheInitializer implements ServletContextListener { private static volatile Map<String, Object> cache; // 共享缓存 @Override public void contextInitialized(ServletContextEvent sce) { // 使用 synchronized 块初始化缓存,避免多个事件并发执行 synchronized (this) { if (cache == null) { cache = new ConcurrentHashMap<>(); // 初始化数据 } } } } ``` - 这在应用启动时常见,防止并发初始化导致的缓存冲突。 4. **WebSocket 或 Netty 处理并发消息**: -使用 WebSocket(如引用[3]中的 Netty 集成)处理实时消息时,消息处理逻辑可能需要同步访问共享状态。 ```java @Component @ServerEndpoint("/ws") public class WebSocketHandler { private static final Object lock = new Object(); // 锁对象 @OnMessage public void handleMessage(String message) { // 使用 synchronized 块保护消息处理逻辑 synchronized (lock) { // 更新全局状态,如用户连接计数 } } } ``` - 在实时通信场景下,确保数据一致性。 这些场景都强调 `synchronized` 在本地 JVM 中的有效性,但当 Spring Boot 涉及事务、缓存或分布式部署时,需额外小心。 --- ### 二、synchronized 的最佳实践 为了避免锁失效或性能瓶颈(如引用[4]中的问题),在 Spring Boot 中使用 `synchronized` 时应遵循以下最佳实践。目标是最小化锁定范围、提升并发效率,并结合 Spring 机制。 1. **优先替代方案,而非依赖 synchronized**: - **使用数据库锁或乐观锁**:在事务方法中,`synchronized` 容易失效(因为它只锁定 JVM 级线程),推荐用 Spring Data JPA 的 `@Lock` 或数据库锁(如 SQL 的 `SELECT FOR UPDATE`)。例如,修复引用[4]的问题: ```java @Repository public interface AccountRepository extends JpaRepository<Account, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) // 使用悲观锁 Account findByAccountNumber(String accountNumber); } @Service public class TransferService { @Autowired private AccountRepository repository; @Transactional public void transfer(String from, String to, int amount) { Account accFrom = repository.findByAccountNumber(from); Account accTo = repository.findByAccountNumber(to); // 执行原子转账操作,避免 synchronized } } ``` - **利用 java.util.concurrent 包**:如使用 `ReentrantLock` 或 `AtomicInteger` 提供更细粒度的控制。例如: ```java @Service public class CounterService { private final Lock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } } ``` 2. **最小化锁定范围**: - 避免锁整个方法(如 `public synchronized void...`),改用 `synchronized` 块锁定特定对象。 ```java public void increment() { // 仅锁定共享变量,减少阻塞时间 synchronized (this) { count++; } } ``` - 使用 `volatile` 关键字:确保变量的可见性,避免指令重排序,适合低争用场景。例如:`private volatile int counter;`。 3. **避免结合 @Transactional**: - 在事务方法中使用 `synchronized` 时,事务提交可能在方法执行后发生(Spring 的代理机制),导致数据库锁不一致(如引用[4]问题)。最佳实践是: - 将事务注解移至外层方法,内层逻辑用 synchronized。 - 或完全移除此组合:如果涉及数据库操作,优先用事务管理器的隔离级别(如 `@Transactional(isolation = Isolation.SERIALIZABLE)`)。 ```java @Service public class CodeService { // 无事务的方法,内部同步生成代码 public synchronized String generateCode() { // 代码生成逻辑 return "unique-code"; } // 外层事务方法调用 @Transactional public void processCode() { String code = generateCode(); // 事务操作 } } ``` 4. **优化并发性能**: - **Bean Scope 管理**:如果服务 Bean 是单例(默认),确保共享状态同步;但如果是原型(prototype),`@Scope("prototype")` 可以减少锁争用。 ```java @Service @Scope("prototype") // 每个请求新实例 public class StatelessService { // 无共享状态,可能不需 synchronized } ``` - **结合缓存机制**:在高并发下,用缓存(如 EhCache 或 Redis,如引用[1])减少数据库访问,降低锁需求。 - **线程池配置**:在 Spring Boot 中优化 Tomcat 线程池(如 `server.tomcat.max-threads`),避免过多线程阻塞在 synchronized 上(参考引用[1])。 5. **测试和监控**: - 使用工具如 JUnit、Jmeter 或 Spring Actuator 测试并发行为(如引用[4]中的 Jmeter 测试)。 - 监控指标:在 Prometheus 或 Log4j 中跟踪锁竞争时间(lock contention),早期发现问题。 --- ### 三、注意事项与常见问题 - **锁失效风险**:在分布式或容器化部署中,多个实例共享数据时,`synchronized` 无效,需用分布式锁(如 Redis Redlock)[^1]。 - **性能陷阱**:过度使用 synchronized 可能导致线程饥饿或死锁(deadlock)。基准测试显示,在高并发下,不必要锁会增加延迟。 - **推荐实践总结**:只在本地 JVM 且低争用场景使用 synchronized;否则,结合 Spring 的并发工具或数据库机制。 --- ### 四、生成相关问题 以下问题扩展了 Spring Boot 并发编程的讨论,帮助你深入理解: 1. 在 Spring Boot 中,如何避免使用 synchronized 时的死锁问题? 2. Spring Boot 分布式系统下有哪些替代 synchronized 的方案? 3. 如何使用 Spring AOP 优化线程同步机制? 4. Spring Boot 事务管理下的乐观锁是如何工作的?[^4] 如果需要更多代码示例或特定场景讨论,请随时提问!作为参考,推荐学习 Java Concurrency in Practice 和 Spring Boot 官方文档。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值