[SpringBoot アプリケーション] SpringBoot は Caffeine ローカル キャッシュを統合します

ローカルキャッシュの概要

キャッシュは日々の開発において重要な役割を果たしており、メモリに保存されるためデータの読み取り速度が非常に速く、データベースへのアクセスが大幅に削減され、データベースへの負荷が軽減されます。

Redis のような NoSql は、キャッシュ コンポーネントとして以前に導入されました。複数のサービス間でキャッシュを提供する分散キャッシュ コンポーネントとして使用できます。ただし、Redis は依然としてネットワーク オーバーヘッドを必要とし、時間の消費が増加します。

ローカル キャッシュはローカル メモリから直接読み取られるため、ネットワーク オーバーヘッドがなく、リモート キャッシュよりもフラッシュ セール システムやデータ量が少ないキャッシュなどに適しています。

ローカル キャッシュ ソリューションの選択

1. ConcurrentHashMap に基づいてローカル キャッシュを実装します。
キャッシュの本質はメモリに保存される KV データ構造であり、これは jdk のスレッドセーフな ConcurrentHashMap に相当します。ただし、キャッシュを実装するには、削除、上限、キャッシュも考慮する必要があります。有効期限の削除およびその他の機能。

利点は、実装が簡単で、サードパーティのパッケージを導入する必要がなく、いくつかの単純なビジネス シナリオにより適していることです。欠点は、より多くの機能が必要な場合、カスタマイズされた開発が必要になり、コストが比較的高くなり、安定性と信頼性を保証するのが難しいことです。より複雑なシナリオの場合は、比較的安定したオープンソース ツールを使用することをお勧めします。

2. Guava Cache に基づいたローカル キャッシュを実装します。Guava
は、Google チームによってオープンソース化された Java コア拡張ライブラリです。これには、コレクション、同時実行プリミティブ、キャッシュ、IO、リフレクションなどのツールボックスが含まれています。パフォーマンスと安定性が保証されており、広く使用されています。使用済み。Guava キャッシュは多くの機能をサポートしています。

  • 最大容量制限をサポート
  • 2 つの有効期限削除戦略 (挿入時間とアクセス時間) をサポートします。
  • 簡単な統計関数をサポート
  • LRUアルゴリズムに基づいて実装

3. Caffeine に基づいたローカル キャッシュの実装
Caffeine は Java8 に基づいた新世代のキャッシュ ツールであり、そのキャッシュ パフォーマンスは理論上の最適値に近いものです。Guava Cache の拡張版とも言え、機能的には似ていますが、Caffeine の方が LRU と LFU の長所を組み合わせたアルゴリズムである W-TinyLFU を使用している点が異なり、性能的には明らかに優れています。

4. Ehcache に基づいたローカル キャッシュの実装
Ehcache は、高速かつ高機能な純粋な Java インプロセス キャッシュ フレームワークであり、Hibernate のデフォルトの CacheProvider です。Caffeine や Guava Cache と比較して、Ehcache はより豊富な機能と強力なスケーラビリティを備えています。

  • LRU、LFU、FIFOを含む複数のキャッシュエビクションアルゴリズムをサポート
  • キャッシュは、ヒープ ストレージ、オフヒープ ストレージ、ディスク ストレージ (永続性をサポート) の 3 種類をサポートします。
  • データ共有の問題を解決するために複数のクラスター ソリューションをサポートする

カフェイン

プロジェクト開発では、システムのパフォーマンスを向上させ、IO オーバーヘッドを削減するために、ローカル キャッシュが不可欠です。最も一般的なローカル キャッシュはグアバとカフェインです。

Caffeine は Google Guava Cache の設計経験をもとに改良したもので、Guava に比べてパフォーマンスとヒット率が向上しており、Guava Plus と考えることができます。

Caffeine は、Java 8 に基づいて開発された高性能のローカル キャッシュ コンポーネントで、ほぼ最高のヒット率を提供しますが、Spring 5 では Guava Cache のサポートが終了し、代わりに Caffeine が使用されます。

カフェインのパフォーマンスは、次のキャッシュ コンポーネントの中で最高です。

ここに画像の説明を挿入します

SpringBoot は 2 つの方法で Caffeine を統合します

SpringBoot には、Caffeine をキャッシュとして使用する 2 つの方法があります。

方法 1: Caffeine の依存関係を直接導入し、Caffeine が提供する API メソッドを使用してローカル キャッシュを実装します。

方法 2: Caffeine と Spring Cache の依存関係を導入し、SpringCache アノテーション メソッドを使用してローカル キャッシュを実装します。

SpringBoot は Caffeine メソッド 1 を統合します

ポンポン

<dependencies>
    <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>
     <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.14</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

アプリケーション.yml

# DataSource Config
spring:
  datasource:
    #   数据源基本配置
    url: jdbc:mysql://localhost:3306/study_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    initialization-mode: always #表示始终都要执行初始化,2.x以上版本需要加上这行配置
    type: com.alibaba.druid.pool.DruidDataSource
    #   数据源其他配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

# Logger Config
logging:
  level:
    cn.zysheep.mapper: debug

キャッシュ構成クラス

@Configuration
public class CaffeineCacheConfig {
    
    
    @Bean
    public Cache<String, Object> caffeineCache() {
    
    
        return Caffeine.newBuilder()
                // 设置最后一次写入或访问后经过固定时间过期
                .expireAfterWrite(60, TimeUnit.SECONDS)
                // 初始的缓存空间大小
                .initialCapacity(100)
                // 缓存的最大条数
                .maximumSize(1000)
                .build();
    }
}

ユーザーエンティティ

@TableName(value ="tb_user")
@Data
public class User implements Serializable {
    
    
    /**
     * 用户ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 姓名
     */
    @TableField("username")
    private String userName;

    /**
     * 现在住址
     */
    @TableField("address")
    private String address;
}

ユーザーマッパー

public interface UserMapper extends BaseMapper<User> {
    
    

}

ユーザーサービス

public interface UserService extends IService<User> {
    
    

    void saveUser(User user);
    
    User getUserById(Long id);

    User updateUser(User user);

    String deleteUserById(Long id);
}
@Service
@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    
    

    private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);


    @Autowired
    private Cache<String, Object> caffeineCache;

    @Override
    public void saveUser(User user) {
    
    
        save(user);
        // 加入缓存
        caffeineCache.put(String.valueOf(user.getId()),user);
    }

    /**
     * 查询用户信息,并缓存结果
     *
     * @param id
     * @return
     */
    public User getUserById(Long id) {
    
    
        // 先从缓存读取
        caffeineCache.getIfPresent(id);
        User user = (User) caffeineCache.asMap().get(String.valueOf(id));
        if (Objects.nonNull(user)) {
    
    
            return user;

        }
        // 如果缓存中不存在,则从库中查找
        user = getById(id);
        // 如果用户信息不为空,则加入缓存
        if (user != null) {
    
    
            caffeineCache.put(String.valueOf(user.getId()), user);
        }

        log.info("从数据库中读取,而非从缓存读取!");
        log.info("users: {}", user);
        return user;
    }


    /**
     * 更新用户信息
     * @param user
     * @return
     */
    public User updateUser(User user) {
    
    
        log.info("user: {}", user);
        updateById(user);
        User user1 = getById(user.getId());
        // 替换缓存中的值
        caffeineCache.put(String.valueOf(user1.getId()), user1);
        return user1;
    }

    public String deleteUserById(Long id) {
    
    
        boolean b = removeById(id);
        if (b) {
    
    
            // 从缓存中删除
            caffeineCache.asMap().remove(String.valueOf(id));
        }
        return b ? "删除成功" : "删除失败";
    }
}

ユーザーコントローラー

@Getter
@Setter
@SuppressWarnings({
    
    "AlibabaClassNamingShouldBeCamel"})
@Accessors(chain = true)
public class R<T> {
    
    
    public static final String DEF_ERROR_MESSAGE = "系统繁忙,请稍候再试";
    public static final String HYSTRIX_ERROR_MESSAGE = "请求超时,请稍候再试";
    public static final int SUCCESS_CODE = 0;
    public static final int FAIL_CODE = -1;
    public static final int TIMEOUT_CODE = -2;
    /**
     * 统一参数验证异常
     */
    public static final int VALID_EX_CODE = -9;
    public static final int OPERATION_EX_CODE = -10;
    /**
     * 调用是否成功标识,0:成功,-1:系统繁忙,此时请开发者稍候再试 详情见[ExceptionCode]
     */
    private int code;

    /**
     * 调用结果
     */
    private T data;

    /**
     * 结果消息,如果调用成功,消息通常为空T
     */
    private String msg = "ok";


    private String path;
    /**
     * 附加数据
     */
    private Map<String, Object> extra;

    /**
     * 响应时间
     */
    private long timestamp = System.currentTimeMillis();

    private R() {
    
    
        super();
    }

    public R(int code, T data, String msg) {
    
    
        this.code = code;
        this.data = data;
        this.msg = msg;
    }

    public static <E> R<E> result(int code, E data, String msg) {
    
    
        return new R<>(code, data, msg);
    }

    /**
     * 请求成功消息
     *
     * @param data 结果
     * @return RPC调用结果
     */
    public static <E> R<E> success(E data) {
    
    
        return new R<>(SUCCESS_CODE, data, "ok");
    }

    public static R<Boolean> success() {
    
    
        return new R<>(SUCCESS_CODE, true, "ok");
    }

    /**
     * 请求成功方法 ,data返回值,msg提示信息
     *
     * @param data 结果
     * @param msg  消息
     * @return RPC调用结果
     */
    public static <E> R<E> success(E data, String msg) {
    
    
        return new R<>(SUCCESS_CODE, data, msg);
    }

    /**
     * 请求失败消息
     *
     * @param msg
     * @return
     */
    public static <E> R<E> fail(int code, String msg) {
    
    
        return new R<>(code, null, (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg);
    }

    public static <E> R<E> fail(String msg) {
    
    
        return fail(OPERATION_EX_CODE, msg);
    }

    public static <E> R<E> fail(String msg, Object... args) {
    
    
        String message = (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg;
        return new R<>(OPERATION_EX_CODE, null, String.format(message, args));
    }

    public static <E> R<E> fail(BaseExceptionCode exceptionCode) {
    
    
        return validFail(exceptionCode);
    }

    public static <E> R<E> fail(BizException exception) {
    
    
        if (exception == null) {
    
    
            return fail(DEF_ERROR_MESSAGE);
        }
        return new R<>(exception.getCode(), null, exception.getMessage());
    }

    /**
     * 请求失败消息,根据异常类型,获取不同的提供消息
     *
     * @param throwable 异常
     * @return RPC调用结果
     */
    public static <E> R<E> fail(Throwable throwable) {
    
    
        return fail(FAIL_CODE, throwable != null ? throwable.getMessage() : DEF_ERROR_MESSAGE);
    }

    public static <E> R<E> validFail(String msg) {
    
    
        return new R<>(VALID_EX_CODE, null, (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg);
    }

    public static <E> R<E> validFail(String msg, Object... args) {
    
    
        String message = (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg;
        return new R<>(VALID_EX_CODE, null, String.format(message, args));
    }

    public static <E> R<E> validFail(BaseExceptionCode exceptionCode) {
    
    
        return new R<>(exceptionCode.getCode(), null,
                (exceptionCode.getMsg() == null || exceptionCode.getMsg().isEmpty()) ? DEF_ERROR_MESSAGE : exceptionCode.getMsg());
    }

    public static <E> R<E> timeout() {
    
    
        return fail(TIMEOUT_CODE, HYSTRIX_ERROR_MESSAGE);
    }


    public R<T> put(String key, Object value) {
    
    
        if (this.extra == null) {
    
    
            this.extra = Maps.newHashMap();
        }
        this.extra.put(key, value);
        return this;
    }

    /**
     * 逻辑处理是否成功
     *
     * @return 是否成功
     */
    public Boolean getIsSuccess() {
    
    
        return this.code == SUCCESS_CODE || this.code == 200;
    }

    /**
     * 逻辑处理是否失败
     *
     * @return
     */
    public Boolean getIsError() {
    
    
        return !getIsSuccess();
    }

    @Override
    public String toString() {
    
    
        return JSONObject.toJSONString(this);
    }
}
@RestController
@RequestMapping("/api")
public class UserController {
    
    

    @Autowired
    private UserService userService;


    @PostMapping("save")
    public R saveUser(@RequestBody User user) {
    
    
        userService.saveUser(user);
        return R.success(null);
    }


    @GetMapping("getById")
    public R getById(@RequestParam Long id) {
    
    
        User user = userService.getUserById(id);
        return R.success(user);
    }

    @GetMapping("getByIdNoCache")
    public R getByNameNoCache(@RequestParam Long id) {
    
    
        List<User> users = userService.getUserByIdNoCache(id);
        return R.success(users);
    }


    @PostMapping("updateUser")
    public R updateUser(User user) {
    
    
        return R.success(userService.updateUser(user));
    }


    @PostMapping("deleteUserById")
    public R deleteUserById(Long id) {
    
    
        return R.success(userService.deleteUserById(id));
    }
}

SpringBoot は Caffeine メソッド 2 を統合します

Caffeine と Spring Cache の依存関係を導入し、SpringCache アノテーション メソッドを使用してローカル キャッシュを実装します。

ポンポン

spring-boot-starter-cacheメソッド 1 の依存関係に依存関係を追加します。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

キャッシュ構成クラス

@Configuration
@EnableCaching
public class CaffeineCacheConfig {
    
    
    @Bean
    public CacheManager cacheManager(){
    
    
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        //Caffeine配置
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                //最后一次写入后经过固定时间过期
                .expireAfterWrite(60*5, TimeUnit.SECONDS)
                //maximumSize=[long]: 缓存的最大条数
                .maximumSize(1000);
        cacheManager.setCaffeine(caffeine);
        return cacheManager;
    }

//    @Bean
//    public Cache<String, Object> caffeineCache() {
    
    
//        return Caffeine.newBuilder()
//                // 设置最后一次写入或访问后经过固定时间过期
//                .expireAfterWrite(60, TimeUnit.SECONDS)
//                // 初始的缓存空间大小
//                .initialCapacity(100)
//                // 缓存的最大条数
//                .maximumSize(1000)
//                .build();
//    }
}

ユーザーサービス

public interface UserService extends IService<User> {

    void saveUser(User user);

    List<User> getUserByIdNoCache(Long id);

    User getUserById(Long id);

    User updateUser(User user);

    List<User> getUserByIdAndName(Long id, String userName);

    List<User> getUser(User user);

    String deleteUserById(Long id);
}
@Service
@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    
    

    private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);


//    @Autowired
//    private Cache<String, Object> caffeineCache;

    @Override
    public void saveUser(User user) {
    
    
        save(user);
        // 加入缓存
       // caffeineCache.put(String.valueOf(user.getId()),user);
    }

    public List<User> getUserByIdNoCache(Long id) {
    
    
        LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery().like(Objects.nonNull(id), User::getId, id);
        List<User> users = list(queryWrapper);
        log.info("从数据库中读取,而非从缓存读取!");
        log.info("users: {}", users);
        return users;
    }



    /**
     * 查询用户信息,并缓存结果
     *
     * @param id
     * @return
     */
    @Cacheable(cacheNames = "user", key = "#id")
    public User getUserById(Long id) {
    
    
        // 先从缓存读取
//        caffeineCache.getIfPresent(id);
//        User user = (User) caffeineCache.asMap().get(String.valueOf(id));
//        if (Objects.nonNull(user)) {
    
    
//            return user;
//
//        }
        // 如果缓存中不存在,则从库中查找
        User user = getById(id);
        // 如果用户信息不为空,则加入缓存
//        if (user != null) {
    
    
//            caffeineCache.put(String.valueOf(user.getId()), user);
//        }

        log.info("从数据库中读取,而非从缓存读取!");
        log.info("users: {}", user);
        return user;
    }

    // spEL使用"T(Type)"来表示 java.lang.Class 实例,"Type"必须是类全限定名,"java.lang"包除外。
    @Cacheable(cacheNames = "user", key = "T(String).valueOf(#id).concat('::').concat(#userName)")
    public List<User> getUserByIdAndName(Long id, String userName) {
    
    
        LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery()
                .like(StringUtils.isNotBlank(userName), User::getUserName, userName)
                .eq(Objects.nonNull(id), User::getId, id);
        List<User> users = list(queryWrapper);
        log.info("从数据库中读取,而非从缓存读取!");
        return users;
    }

    @Cacheable(cacheNames = "user", key = "#user.userName")
    public List<User> getUser(User user) {
    
    
        LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery().like(StringUtils.isNotBlank(user.getUserName()), User::getUserName, user.getUserName());
        List<User> users = list(queryWrapper);
        log.info("从数据库中读取,而非从缓存读取!");
        return users;
    }

    /**
     * 更新用户信息
     *
     * @param user
     * @return
     */
    @CachePut(cacheNames = "user", key = "#result.id")
    public User updateUser(User user) {
    
    
        log.info("user: {}", user);
        updateById(user);
        User user1 = getById(user.getId());


        // 替换缓存中的值
        // caffeineCache.put(String.valueOf(user1.getId()), user1);

        return user1;
    }

    @CacheEvict(cacheNames = "user", beforeInvocation = true, key = "#id")
    public String deleteUserById(Long id) {
    
    
        boolean b = removeById(id);
//        if (b) {
    
    
//            // 从缓存中删除
//            caffeineCache.asMap().remove(String.valueOf(id));
//        }
        //  int i = 1 / 0;
        return b ? "删除成功" : "删除失败";
    }
}

ラベルキャッシュの注釈

  • @Cacheable: @Cacheble アノテーションは、このメソッドにキャッシュの機能があることを示します。メソッドの戻り値はキャッシュされます。次回このメソッドを呼び出す前に、キャッシュに値がすでに存在するかどうかがチェックされます。存在する場合は、メソッドを呼び出さずに直接戻ります。そうでない場合は、メソッドが呼び出され、結果がキャッシュされます。このアノテーションは通常、クエリ メソッドで使用されます。
  • @CacheEvict: @CacheEvict アノテーションが付けられたメソッドは、指定されたキャッシュをクリアします。通常、更新または削除メソッドで使用されます。
  • @CachePut: @CachePut アノテーションが付けられたメソッドは、メソッドが確実に呼び出され、結果がキャッシュされることを期待します。メソッドの戻り値はキャッシュに入れられ、キャッシュされます。通常、新しいメソッドに使用されます。
  • @Caching: 複雑なキャッシュ ルールを定義する
  • @CacheConfig: キャッシュのパブリック構成を抽出します。

アノテーションの具体的な使用法: [SpringBoot Advanced] SpringBoot 統合キャッシュ ローカル キャッシュ

おすすめ

転載: blog.csdn.net/qq_45297578/article/details/132806735