cacheable
基于scala+zio的分布式缓存工具,功能类似Spring缓存注解@Cacheable 、@CacheEvict 。支持使用caffeine和zio-redis作为存储介质。
我的目标是为scala+zio应用提供一层无侵入性、可替换、可配置、类型安全的缓存工具,不受限于存储介质,容易使用。适用于读多写少的场景,减少数据库的压力(其中Redis缓存更新中暂没有使用锁,有覆盖可能),由于应用本身是基于zio/zstream的,如果不考虑数据库压力(或JDBC与cacheable性能无区别),则没有必要使用cacheable 。
其中,缓存均使用hash存储,key为类名+方法名,field为方法的所有参数 value为方法的返回值。
缓存的语义
- cacheable — 查缓存,有则返回,无则查库并设置缓存
- local 指定是用内存作为缓存存储,默认为
true - cacheEvict — 删除缓存中的某个方法的所有记录
- local 指定是用内存作为缓存存储,默认为
true - values 指定需要清除的查询方法名 编译期检查,必须与注解所在类是同一个密闭类中成员;未指定values或者为
Nil ,则清除该密闭类中的所有方法的缓存
如何设计
根据我们的目标:
- 拓展性:存储和API隔离,不使用则不需要导入
- 安全性:尽可能编译期检查,尽快发现错误
- 便捷性:提供默认实现,且用户能使用自定义实现替换默认实现
- 合理性:尽可能使用与spring相同的语义
- 存储解构:使用hash,能一键清理key的所有缓存值
- 内部实现原理:implicit域内搜索,编译期代码合成,元/宏编程
如果从cache中查数据比ZIO.effect 中的操作快很多,则缓存才是有意义的,否则没必要使用。
@cacheable API设计
import zio.ZIO
import zio.stream.ZStream
trait Cache[Z] {
def getIfPresent(business: => Z)(identities: List[String], args: List[_]): Z
def cacheKey(keys: List[String]): String = keys.mkString("-")
def cacheField(args: List[_]): String = args.map(_.toString).mkString("-")
}
object Cache {
def apply[R, E, T](business: => ZStream[R, E, T])(identities: List[String], args: List[_])(implicit streamCache: ZStreamCache[R, E, T]): ZStream[R, E, T] = {
streamCache.getIfPresent(business)(identities, args)
}
def apply[R, E, T](business: => ZIO[R, E, T])(identities: List[String], args: List[_])(implicit cache: ZIOCache[R, E, T]): ZIO[R, E, T] = {
cache.getIfPresent(business)(identities, args)
}
}
使用API
以使用def apply[R, E, T](business: => ZIO[R, E, T])(identities: List[String], args: List[_])(implicit cache: ZIOCache[R, E, T]): ZIO[R, E, T] 为例,我们需要在域内提供隐式参数implicit cache: ZIOCache[R, E, T] ,
def readIOFunction(id: Int, key: String): ZIO[Any, Throwable, String] = {
val $result = ZIO.effect("hello world" + Random.nextInt())
Cache($result)(List("UseCaseExample", "readIOFunction"), List(id, key))
}
工具自带实现,只需要导入即可,如使用Redis导入import org.bitlap.cacheable.redis.Implicits._ ,使用caffeine导入import org.bitlap.cacheable.redis.Implicits._ 。如下想使用自己定义的就需要实现一个ZIOCache 或ZStreamCache 实例作为隐式参数。同时由于作用域的原因,当前域内定义的会覆盖import 导入的。
使用@cacheable 注解自动生成
@cacheable(local = true)
def readStreamFunction(id: Int, key: String): ZStream[Any, Throwable, String] = {
ZStream.fromEffect(ZIO.effect(s"hello world--$id-$key-${Random.nextInt()}"))
}
完整例子:smt-cacheable-examples
这个注解宏逻辑比较简单,核心逻辑只有二十行左右的代码:
def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = {
val resTree = annottees.map(_.tree) match {
case (defDef @ DefDef(mods, name, tparams, vparamss, tpt, _)) :: Nil =>
if (tpt.isEmpty) {
c.abort(c.enclosingPosition, "The return type of the method is not specified!")
}
val tp = c.typecheck(tq"$tpt", c.TYPEmode).tpe
if (!(tp <:< typeOf[zio.ZIO[_, _, _]]) && !(tp <:< typeOf[zio.stream.ZStream[_, _, _]])) {
c.abort(c.enclosingPosition, s"The return type of the method not support type: `${tp.typeSymbol.name.toString}`!")
}
val importExpr = if (local) q"import _root_.org.bitlap.cacheable.caffeine.Implicits._" else q"import _root_.org.bitlap.cacheable.redis.Implicits._"
val newBody =
q"""
val $resultValName = ${defDef.rhs}
val $keyValName = List($getEnclosingClassName, ${name.decodedName.toString})
$importExpr
org.bitlap.cacheable.core.Cache($resultValName)($keyValName, ..${getParamsName(vparamss)})
"""
DefDef(mods, name, tparams, vparamss, tpt, newBody)
}
printTree(force = true, resTree)
c.Expr[Any](resTree)
}
总结
本身对于一个纯异步应用而已,使用cache的场景是很少的,这里更多的是使用macro和implicit设计出灵活的工具。就这个cacheable 本身而言,个人认为用处不大。不过思想可以复用。
|