22、扩展函数与高阶函数在缓存中的应用:内存缓存的DSL封装、磁盘缓存的扩展函数、多级缓存策略

缓存这东西,说白了就是拿空间换时间。我在项目里见过不少团队,一开始觉得缓存简单,不就是存个数据嘛。结果业务一复杂,内存、磁盘、网络三层搅在一起,代码写得跟意大利面条似的。今天我们就用Kotlin的扩展函数和高阶函数,把缓存这件事理清楚。

为什么缓存需要DSL和扩展函数?

你想想看,传统的缓存代码长什么样?

// 传统写法
val cache = MemoryCache()
val data = cache.get("user_123")
if (data == null) {
    val newData = fetchFromNetwork()
    cache.put("user_123", newData)
}

这种写法有什么问题?每次都要手动检查null、手动回填。业务一多,到处都是重复的模板代码。我个人习惯用扩展函数把这些逻辑封装起来,调用方只需要关心「我要什么数据」,剩下的交给函数内部处理。

内存缓存的DSL封装

先看内存缓存。Kotlin的DSL能力特别适合用来配置缓存策略——过期时间、最大容量、淘汰策略,这些参数用DSL写出来,读起来就像在描述需求。

class MemoryCache<K, V> {
    private val map = LinkedHashMap<K, CacheEntry<V>>()
    private var maxSize: Int = 100
    private var expireTime: Long = 60_000L // 默认60秒

    // DSL配置函数
    fun config(block: MemoryCache<K, V>.() -> Unit) {
        block()
    }

    fun maxSize(size: Int) {
        this.maxSize = size
    }

    fun expireTime(millis: Long) {
        this.expireTime = millis
    }

    fun put(key: K, value: V) {
        if (map.size >= maxSize) {
            val oldest = map.keys.first()
            map.remove(oldest)
        }
        map[key] = CacheEntry(value, System.currentTimeMillis())
    }

    fun get(key: K): V? {
        val entry = map[key] ?: return null
        if (System.currentTimeMillis() - entry.timestamp > expireTime) {
            map.remove(key)
            return null
        }
        return entry.value
    }

    private data class CacheEntry<V>(val value: V, val timestamp: Long)
}

// 使用DSL配置
val userCache = MemoryCache<String, User>().apply {
    config {
        maxSize(200)
        expireTime(120_000L)
    }
}

嗯,这里要注意一点:DSL的config块其实就是一个高阶函数,它接收一个MemoryCache.() -> Unit类型的lambda。这种写法让配置变得特别直观,团队成员一看就知道这个缓存能存多少、过期多久。

我的经验:DSL配置的maxSize和expireTime最好提供默认值。我在项目中遇到过有人忘了调config,结果缓存一直用默认的100条、60秒过期,上线后才发现热点数据频繁被淘汰。给个合理的默认值,至少不会出大问题。

磁盘缓存的扩展函数

磁盘缓存跟内存缓存不一样,它涉及序列化、文件读写、IO异常处理。用扩展函数可以把这些脏活累活藏起来。

// 为任意类型添加磁盘缓存能力
inline fun <reified T> Any.diskCache(
    key: String,
    cacheDir: File,
    expireTime: Long = 24 * 60 * 60_000L,
    crossinline fetcher: () -> T?
): T? {
    val cacheFile = File(cacheDir, key.hashCode().toString())
    
    // 先读磁盘
    if (cacheFile.exists()) {
        val lastModified = cacheFile.lastModified()
        if (System.currentTimeMillis() - lastModified < expireTime) {
            try {
                val bytes = cacheFile.readBytes()
                return deserialize<T>(bytes)
            } catch (e: Exception) {
                cacheFile.delete() // 损坏就删掉
            }
        } else {
            cacheFile.delete() // 过期就删掉
        }
    }
    
    // 磁盘没有,执行fetcher获取数据
    val data = fetcher()
    if (data != null) {
        try {
            val bytes = serialize(data)
            cacheFile.writeBytes(bytes)
        } catch (e: Exception) {
            // 写磁盘失败,不影响业务
        }
    }
    return data
}

// 序列化/反序列化辅助函数
inline fun <reified T> serialize(value: T): ByteArray {
    // 这里可以用Gson、Kotlin Serialization等
    return value.toString().toByteArray()
}

inline fun <reified T> deserialize(bytes: ByteArray): T? {
    // 反序列化实现
    return null
}

这个扩展函数有意思的地方在于:它是挂载在Any类型上的。也就是说,任何对象都能调用diskCache。我曾经在一个项目里直接用activity.diskCache("user_list")来缓存列表数据,写起来特别顺手。

避坑指南:扩展函数里的crossinline不能少。因为fetcher是一个lambda,可能会在非局部返回的场景下使用。不加crossinline,编译器会报错。我曾经在这个坑里卡了半小时,最后发现是lambda的返回标签问题。

多级缓存策略

单级缓存不够用怎么办?内存+磁盘+网络,三级联动才是生产级的方案。我们用高阶函数把这三层串起来。

class MultiLevelCache<K, V>(
    private val memoryCache: MemoryCache<K, V>,
    private val diskCacheDir: File
) {
    // 核心:多级缓存读取
    fun get(
        key: K,
        expireTime: Long = 60_000L,
        fetcher: () -> V?
    ): V? {
        // 第一级:内存
        memoryCache.get(key)?.let {
            return it
        }
        
        // 第二级:磁盘
        val diskKey = key.toString()
        val diskResult = diskCache<V>(diskKey, diskCacheDir, expireTime) {
            // 如果磁盘也没有,才执行fetcher
            fetcher()
        }
        
        if (diskResult != null) {
            // 回填内存
            memoryCache.put(key, diskResult)
            return diskResult
        }
        
        // 第三级:网络(由fetcher触发)
        val networkResult = fetcher()
        if (networkResult != null) {
            memoryCache.put(key, networkResult)
            // 异步写磁盘
            Thread {
                diskCache<V>(diskKey, diskCacheDir, expireTime) { networkResult }
            }.start()
        }
        return networkResult
    }
}

// 使用示例
val cache = MultiLevelCache<String, User>(memoryCache, diskDir)
val user = cache.get("user_123", expireTime = 120_000L) {
    // 这里放网络请求
    api.fetchUser("123")
}

这个设计的关键在于:每一级缓存都只关心自己的职责。内存管快,磁盘管持久,网络管最新。调用方只需要传一个fetcher,剩下的各级缓存自动协作。

核心思想:多级缓存不是简单的if-else嵌套,而是用高阶函数把「查找-回填」的逻辑抽象成一条流水线。每一级都尝试返回数据,如果命中就提前终止;如果没命中,就调用下一级,同时把结果回填到上一级。

知识体系总览

下面这张图把本章的核心逻辑串起来了,你可以看到扩展函数和高阶函数在每一层扮演的角色。

多级缓存策略 — 扩展函数与高阶函数应用 调用方 传入 fetcher lambda MultiLevelCache.get() 高阶函数编排三级缓存 内存缓存 DSL配置 · 快速存取 磁盘缓存 扩展函数 · 序列化/反序列化 网络请求 fetcher 回调 · 异步回填 回填上一级缓存(内存 ← 磁盘 ← 网络) 扩展函数封装细节 · 高阶函数串联流程 · DSL配置策略

从这张图可以看得很清楚:调用方只跟MultiLevelCache.get()打交道,传入一个fetcher。内部用高阶函数依次尝试内存、磁盘、网络三级,命中就返回,没命中就继续往下走,同时把结果回填到上一级。每一级的实现细节——内存的DSL配置、磁盘的扩展函数——都被封装得干干净净。

总结一下

扩展函数和高阶函数在缓存中的应用,本质上是在做一件事:把「怎么存」和「怎么取」的细节,从业务代码中剥离出去。内存缓存用DSL配置参数,磁盘缓存用扩展函数处理序列化,多级缓存用高阶函数串联流程。每一层都只暴露最简洁的接口给调用方。

我在项目中实践这套方案后,缓存相关的bug减少了至少一半。以前经常出现的「缓存穿透」「缓存雪崩」问题,因为有了统一的过期时间和淘汰策略,基本没再出现过。说白了,好的设计不是功能多强大,而是让使用者不容易犯错。


公众号:蓝海资料掘金营,微信deep3321