面试题答案
一键面试利用Java线程池设计缓存系统提升命中率
- 线程池的选择:
- 对于电商系统缓存模块,由于读写操作频繁且并发度高,可以选择
ThreadPoolExecutor
。例如,可以根据系统的硬件资源和预估的并发量来设置核心线程数corePoolSize
、最大线程数maximumPoolSize
。 - 核心线程数可以设置为CPU核心数的一定倍数(如2倍),以充分利用CPU资源。最大线程数可以根据系统的最大并发承受能力进行调整,同时要考虑系统的内存等资源限制。
- 设置合适的队列
BlockingQueue
,如LinkedBlockingQueue
,来缓冲任务。队列大小也需要根据实际情况设置,避免队列过大导致内存溢出或过小导致任务拒绝。
- 对于电商系统缓存模块,由于读写操作频繁且并发度高,可以选择
- 缓存读写操作:
- 读操作:
- 可以将读缓存操作封装成任务提交到线程池。在线程池中,首先检查缓存中是否存在所需商品数据。如果存在,直接返回缓存数据,提升缓存命中率。
- 例如,定义一个
CacheReaderTask
类实现Runnable
接口,在run
方法中进行缓存读取逻辑。
- 写操作:
- 同样将写缓存操作封装成任务提交到线程池。当有新的商品数据需要缓存时,线程池中的线程负责将数据写入缓存。可以采用异步写入的方式,减少对主线程的阻塞,提高系统整体性能。
- 例如,定义一个
CacheWriterTask
类实现Runnable
接口,在run
方法中进行缓存写入逻辑。
- 读操作:
- 缓存数据结构:
- 可以使用
ConcurrentHashMap
来存储缓存数据,它支持高并发的读写操作,保证在多线程环境下的数据一致性和高性能。对于不同类型的商品数据,可以采用不同的ConcurrentHashMap
实例或者通过合适的前缀来区分,便于管理和维护。
- 可以使用
处理缓存穿透问题
- 布隆过滤器:
- 在缓存读取之前,先通过布隆过滤器判断数据是否存在。布隆过滤器可以快速判断一个元素是否在一个集合中,误判率较低。
- 当有读请求时,先查询布隆过滤器,如果布隆过滤器判断数据不存在,直接返回,不再查询数据库,避免无效的数据库查询,从而防止缓存穿透。
- 例如,可以使用Google的Guava库中的
BloomFilter
来实现。在商品数据写入缓存时,同时将数据的唯一标识(如商品ID)添加到布隆过滤器中。
- 空值缓存:
- 当查询数据库发现数据不存在时,仍然将一个空值(如
null
)缓存起来,并设置一个较短的过期时间。这样下次相同的查询请求就可以直接从缓存中获取空值,而不会再次查询数据库。 - 例如,在
CacheReaderTask
中,如果查询数据库返回null
,则将null
值写入缓存,并设置过期时间为1分钟。
- 当查询数据库发现数据不存在时,仍然将一个空值(如
处理缓存雪崩问题
- 缓存过期时间打散:
- 对于大量的商品缓存数据,设置不同的过期时间,避免所有缓存同时过期。可以在一个合理的时间范围内(如1 - 24小时),随机为每个商品缓存设置过期时间。
- 例如,在
CacheWriterTask
中,使用Random
类生成一个随机的过期时间(单位可以是秒),然后设置到缓存数据中。
- 互斥锁:
- 当缓存过期后,在查询数据库并重新构建缓存时,可以使用互斥锁(如
ReentrantLock
)来保证同一时间只有一个线程去查询数据库和更新缓存。其他线程等待该线程更新完缓存后,直接从缓存中获取数据。 - 例如,在
CacheReaderTask
中,当发现缓存过期时,先获取互斥锁,查询数据库更新缓存后释放锁。其他线程在获取锁失败时,等待一段时间后再次尝试从缓存获取数据。
- 当缓存过期后,在查询数据库并重新构建缓存时,可以使用互斥锁(如