キャッシュは、頻繁にアクセスされるデータをメモリに保存し、データベースなどの基盤となるデータ ソースへの負担を軽減することで、システムのパフォーマンスと安定性を効果的に向上させることができます。誰もがプロジェクトで多かれ少なかれこれを使用したことがあると思いますが、私たちのプロジェクトも例外ではありませんでした。しかし、最近会社のコードをレビューしていたとき、その記述は非常に愚かで低レベルでした。大まかな記述は次のとおりです:
public User getById(String id) { User user = cache.getUser(); if(user != null) { return user; } // 从数据库获取 user = loadFromDB(id); cahce.put(id, user); return user; }
実際、Spring Boot は、アプリケーションにキャッシュを簡単に追加できる強力なキャッシュ抽象化を提供します。この記事では、Spring が提供するさまざまなキャッシュ アノテーションを使用してキャッシュのベスト プラクティスを実装する方法について説明します。
現在、ほとんどのプロジェクトは SpringBoot プロジェクトなので、アノテーション @EnableCaching
をスタートアップ クラスに追加して、キャッシュ機能を有効にすることができます。
@SpringBootApplication @EnableCaching public class SpringCacheApp { public static void main(String[] args) { SpringApplication.run(Cache.class, args); } }
キャッシュを使用できるようにしたいので、キャッシュ マネージャー Bean が必要です。デフォルトでは、@EnableCaching
は ConcurrentMapCacheManager
Bean を登録します。いいえ個別の Bean 宣言。 ConcurrentMapCacheManage
r は、値を ConcurrentHashMap
のインスタンスに保存します。これは、キャッシュ メカニズムの最も単純なスレッドセーフな実装です。
デフォルトのキャッシュ マネージャーは JVM メモリに保存されているためニーズを満たすことができません。では、Redis に保存するにはどうすればよいでしょうか?現時点では、カスタム キャッシュ マネージャーを追加する必要があります。
1. 依存関係を追加する
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2. Redis キャッシュ マネージャーを構成する
@Configuration @EnableCaching public class CacheConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(); } @Bean public CacheManager cacheManager() { RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .disableCachingNullValues() .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory()) .cacheDefaults(redisCacheConfiguration) .build(); return redisCacheManager; } }
キャッシュ マネージャーを用意したところで、ビジネス レベルでキャッシュを操作するにはどうすればよいでしょうか?
@Cacheable
、@CachePut
、または @CacheEvict
アノテーションを使用してキャッシュを操作できます。
このアノテーションはメソッドの実行結果をキャッシュできます。キャッシュ制限時間内に再度メソッドが呼び出された場合、メソッド自体は呼び出されませんが、結果が取得されます。キャッシュから直接取得され、呼び出し元に返されます。
例 1: データベース クエリの結果をキャッシュします。
@Service public class MyService { @Autowired private MyRepository repository; @Cacheable(value = "myCache", key = "#id") public MyEntity getEntityById(Long id) { return repository.findById(id).orElse(null); } }
この例では、@Cacheable
アノテーションを使用して、ID
に基づいて getEntityById()
メソッドの結果をキャッシュします。データベースから MyEntity オブジェクトを取得します。
しかし、データを更新したらどうなるでしょうか?古いデータがまだキャッシュにありますか?
その後、@CachePut
が出てきました。@Cacheable
アノテーションとの違いは、@CachePut## を使用することです。 # annotation annotation. このメソッドは、実行前にキャッシュ内に以前に実行された結果があるかどうかを確認しません。代わりに、メソッドは毎回実行され、実行結果はキーと値のペアの形式で指定されたキャッシュに書き込まれます。
@CachePut アノテーションは通常、キャッシュデータを更新するために使用されます。これは、書き込みモードでの二重書き込みモードを使用するキャッシュに相当します。
@Service public class MyService { @Autowired private MyRepository repository; @CachePut(value = "myCache", key = "#entity.id") public void saveEntity(MyEntity entity) { repository.save(entity); } }
@CacheEvict とマークされたメソッドは、呼び出されたときに、保存されているデータをキャッシュから削除します。
@CacheEvict アノテーションは通常、キャッシュされたデータを削除するために使用されます。これは、書き込みモードで障害モードを使用するキャッシュと同等です。
@Service public class MyService { @Autowired private MyRepository repository; @CacheEvict(value = "myCache", key = "#id") public void deleteEntityById(Long id) { repository.deleteById(id); } }
@Caching アノテーションは、メソッドまたはクラスで複数の Spring Cache 関連のものを同時に指定するために使用されます。時間の注釈。
@Caching アノテーションで指定された
evict 属性は、メソッド
saveEntity## が実行されると無効になります。 # は 2 つのキャッシュと呼ばれます。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:java;">@Service
public class MyService {
@Autowired
private MyRepository repository;
@Cacheable(value = "myCache", key = "#id")
public MyEntity getEntityById(Long id) {
return repository.findById(id).orElse(null);
}
@Caching(evict = {
@CacheEvict(value = "myCache", key = "#entity.id"),
@CacheEvict(value = "otherCache", key = "#entity.id")
})
public void saveEntity(MyEntity entity) {
repository.save(entity);
}
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
例 2:
メソッドを呼び出すと、Spring はまず結果が myCache
キャッシュにキャッシュされているかどうかを確認します。その場合、Spring
はメソッドを実行する代わりに、キャッシュされた結果を返します。結果がまだキャッシュされていない場合、Spring はメソッドを実行し、結果を myCache
キャッシュにキャッシュします。メソッドの実行後、Spring は @CacheEvict
アノテーションに基づいて、キャッシュされた結果を otherCache
キャッシュから削除します。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:java;">@Service
public class MyService {
@Caching(
cacheable = {
@Cacheable(value = "myCache", key = "#id")
},
evict = {
@CacheEvict(value = "otherCache", key = "#id")
}
)
public MyEntity getEntityById(Long id) {
return repository.findById(id).orElse(null);
}
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
例 3:
メソッドを呼び出すと、Spring はまず @CacheEvict
アノテーションに基づいて otherCache
キャッシュからデータを削除します。 Spring はメソッドを実行し、結果をデータベースまたは外部 API に保存します。 メソッドが実行されると、Spring は
myCache、
myOtherCache、および
myThirdCache キャッシュに追加します。 CachePut
アノテーション。中央。 Spring は、@Cacheable
アノテーションに基づいて、結果が myFourthCache
および myFifthCache
キャッシュにキャッシュされているかどうかもチェックします。結果がまだキャッシュされていない場合、Spring は結果を適切なキャッシュにキャッシュします。結果がすでにキャッシュされている場合、Spring はメソッドを再度実行する代わりに、キャッシュされた結果を返します。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:java;">@Service
public class MyService {
@Caching(
put = {
@CachePut(value = "myCache", key = "#result.id"),
@CachePut(value = "myOtherCache", key = "#result.id"),
@CachePut(value = "myThirdCache", key = "#result.name")
},
evict = {
@CacheEvict(value = "otherCache", key = "#id")
},
cacheable = {
@Cacheable(value = "myFourthCache", key = "#id"),
@Cacheable(value = "myFifthCache", key = "#result.id")
}
)
public MyEntity saveData(Long id, String name) {
// Code to save data to a database or external API
MyEntity entity = new MyEntity(id, name);
return entity;
}
}</pre><div class="contentsignin">ログイン後にコピー</div></div><h3>@CacheConfig</h3><p>通过<code>@CacheConfig
注解,我们可以将一些缓存配置简化到类级别的一个地方,这样我们就不必多次声明相关值:
@CacheConfig(cacheNames={"myCache"}) @Service public class MyService { @Autowired private MyRepository repository; @Cacheable(key = "#id") public MyEntity getEntityById(Long id) { return repository.findById(id).orElse(null); } @CachePut(key = "#entity.id") public void saveEntity(MyEntity entity) { repository.save(entity); } @CacheEvict(key = "#id") public void deleteEntityById(Long id) { repository.deleteById(id); } }
condition
作用:指定缓存的条件(满足什么条件才缓存),可用 SpEL
表达式(如 #id>0
,表示当入参 id 大于 0 时才缓存)
unless
作用 : 否定缓存,即满足 unless
指定的条件时,方法的结果不进行缓存,使用 unless
时可以在调用的方法获取到结果之后再进行判断(如 #result == null,表示如果结果为 null 时不缓存)
//when id >10, the @CachePut works. @CachePut(key = "#entity.id", condition="#entity.id > 10") public void saveEntity(MyEntity entity) { repository.save(entity); } //when result != null, the @CachePut works. @CachePut(key = "#id", condition="#result == null") public void saveEntity1(MyEntity entity) { repository.save(entity); }
通过allEntries
、beforeInvocation
属性可以来清除全部缓存数据,不过allEntries
是方法调用后清理,beforeInvocation
是方法调用前清理。
//方法调用完成之后,清理所有缓存 @CacheEvict(value="myCache",allEntries=true) public void delectAll() { repository.deleteAll(); } //方法调用之前,清除所有缓存 @CacheEvict(value="myCache",beforeInvocation=true) public void delectAll() { repository.deleteAll(); }
Spring Cache注解中频繁用到SpEL表达式,那么具体如何使用呢?
SpEL 表达式的语法
Spring Cache可用的变量
通过Spring
缓存注解可以快速优雅地在我们项目中实现缓存的操作,但是在双写模式或者失效模式下,可能会出现缓存数据一致性问题(读取到脏数据),Spring Cache
暂时没办法解决。最后我们再总结下Spring Cache使用的一些最佳实践。
只缓存经常读取的数据:缓存可以显着提高性能,但只缓存经常访问的数据很重要。很少或从不访问的缓存数据会占用宝贵的内存资源,从而导致性能问题。
根据应用程序的特定需求选择合适的缓存提供程序和策略。SpringBoot
支持多种缓存提供程序,包括 Ehcache
、Hazelcast
和 Redis
。
使用缓存时请注意潜在的线程安全问题。对缓存的并发访问可能会导致数据不一致或不正确,因此选择线程安全的缓存提供程序并在必要时使用适当的同步机制非常重要。
避免过度缓存。缓存对于提高性能很有用,但过多的缓存实际上会消耗宝贵的内存资源,从而损害性能。在缓存频繁使用的数据和允许垃圾收集不常用的数据之间取得平衡很重要。
使用适当的缓存逐出策略。使用缓存时,重要的是定义适当的缓存逐出策略以确保在必要时从缓存中删除旧的或陈旧的数据。
使用适当的缓存键设计。缓存键对于每个数据项都应该是唯一的,并且应该考虑可能影响缓存数据的任何相关参数,例如用户 ID、时间或位置。
常规数据(读多写少、即时性与一致性要求不高的数据)完全可以使用 Spring Cache,至于写模式下缓存数据一致性问题的解决,只要缓存数据有设置过期时间就足够了。
特殊数据(读多写多、即时性与一致性要求非常高的数据),不能使用 Spring Cache,建议考虑特殊的设计(例如使用 Cancal 中间件等)。
以上がSpringBootプロジェクトでキャッシュを使用する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。