持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情
背景
项目中我们经常会用缓存来提升系统的性能,但是一旦提到缓存,大多数人都会想到的是Redis和MemCache。其实缓存可以分为本地缓存和分布式缓存。
通过上图我们知道分布式缓存和本地缓存的优缺点,然而本地缓存也经过了几代的优化,由第一代的Encache、经过一段时间到第二代本地缓存Guava Cache,在到最新的缓存Caffeine,每一代的缓存都经过相关优化和性能的提升。
简介
Caffeine是基于java8实现的新一代缓存工具,缓存性能接近理论最优。不同的是Caffeine采用了一种结合LRU、LFU优点的算法:W-TinyLFU,在性能上有明显的提升。
原理
常见的缓存策略
FIFO(First In First Out) 先进先出
它是优先淘汰掉最先缓存的数据、是最简单的淘汰算法。缺点是如果先缓存的数据使用频率比较高的话,那么该数据就不停地进进出出,因此它的缓存命中率比较低。这也就是caffenie所描述的近乎最佳的命中率
LRU(Least Recently Used):最近最久未使用。
它是优先淘汰掉最久未访问到的数据。缺点是不能很好地应对偶然的突发流量。有一批数据在59秒都没有被访问,然后到1分钟的时候过期了,但在1分01秒来了大批量的访问,导致穿透缓存,打垮数据库。
LFU(Least Frequently Used):最少使用
最近最少频率使用。它是优先淘汰掉最不经常使用的数据,需要维护一个表示使用频率的字段。 就凭这个字段对于缓存来说就是很不友好的,因为我们的内存资源是真的很可贵的。
而Caffeine使用的是W-TinyLFU 算法。
W-TinyLFU 算法简介
传统的LFU受时间周期的影响比较大。所以各种LFU的变种出现了,基于时间周期进行衰减,或者在最近某个时间段内的频率。同样的LFU也会使用额外空间记录每一个数据访问的频率,即使数据没有在缓存中也需要记录,所以需要维护的额外空间很大。
W-TinyLFU结合了LRU和LFU,以及其他的算法的一些特点,优化了其他算法的相关缺陷。
系统集成
引入jar包
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
说明:SpringBoot默认已经集成了Caffeine,只需要添加相关依赖即可,无需指定版本号。
缓存相关配置
@Configuration
public class CacheConfig
{
@Bean
public Cache<String, Object> creatCaffeineCache() {
return Caffeine.newBuilder()
//设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(60, TimeUnit.SECONDS)
//初始化缓存空间大小
.initialCapacity(100)
//最大缓存数
.maximumSize(1000)
.build();
}
}
说明:
- initialCapacity:初始的缓存空间大小
- maximumSize:缓存的最大条数
- expireAfterWrite:最后一次写入后经过固定时间过期
- refreshAfterWrite:写入后经过固定的时间刷新缓存
- expireAfterAccess :最后一次写入或访问后经过固定时间过期
expireAfterWrite 和 expireAfterAccess同时设置时,以expireAfterWrite为标准。
缓存的顶级接口
public interface CacheService<T>
{
void put(String key ,T value);
T get(String key);
}
具体实现
@Service("CaffeineCacheService")
public class CaffeineCacheServiceImpl<T> implements CacheService<T>
{
@Resource
private Cache<String, T> caffeineCache;
/**
* 获取缓存对象
* @param key
* @return
*/
public T get(String key)
{
return caffeineCache.asMap().get(key);
}
public void put(String key ,T value)
{
caffeineCache.asMap().put(key, value);
}
}
说明:设计顶级接口,其他系统只需要调用Api接口方法,无需关注具体的实现。
具体业务调用缓存接口
@Service
@Transactional(rollbackFor=Exception.class)
public class UserServiceImpl implements UserService
{
private Logger logger=LoggerFactory.getLogger(UserServiceImpl.class);
@Qualifier("CaffeineCacheService")
@Autowired
private CacheService<User> cacheService;
@Autowired
private UserMapper userMapper;
@Override
public User getUserById(String userId)
{
User cacheUser =cacheService.get(userId);
if(cacheUser!=null)
{
logger.info("get date in cache");
return cacheUser;
}
User dbUser=userMapper.getUserById(userId);
if(dbUser!=null)
{
logger.info("put data into cache");
cacheService.put(userId, dbUser);
}
return dbUser;
}
}
说明:先通过缓存查询,如果缓存中不存在,则在查询数据库,再将数据库的信息添加到缓存中。
测试示例代码
@RestController
@RequestMapping("/user")
public class UserController
{
private Logger logger=LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
@GetMapping("getUserById")
public User getUserById(@RequestParam String userId)
{
logger.info("getUserByName paramter userId:"+userId);
return userService.getUserById(userId);
}
}
测试结果
第一次访问接口日志的信息如下:
说明:第一次查询缓存中未存储数据,所以先查询数据库,在将数据添加到缓存中。
第二次访问接口日志的信息如下:
说明:缓存中已经存在数据,查询结果直接从缓存中获取。
总结
本文讲解了Spring Boot集成高效缓存(Caffeine)的使用,如有疑问请随时反馈。