为什么有plumecache?
日常项目开发中使用缓存的时候,无论选择redis、memcached,或是其它类型缓存,我们总要在做一些封装,才能很好的使用。plumecache是我个人缓存使用经验的一些总结,旨在于解决日常使用缓存时的一些问题和痛点,写出来和大家分享,也欢迎大家的star和issue Gitee地址
简介
Plumecache 是一个常见分布式缓存封装工具类库,降低相关client api学习成本,提高工作效率,当前支持reids(standalone,sentinel,cluster)、memcached
Plumecache解决什么问题?
- 1.用于替换项目中CacheService、CacheHelper、CacheHandler、CacheUtils等缓存工具类或胶水代码
- 2.支持多缓存实例管理,解决既要还要的问题
- 3.支持缓存key前缀、缓存版本的配置
- 4.支持缓存序列化统一处理,不需要在每个调用的地方写序列化,支持自定义
- 5.支持大缓存压缩处理,支持自定义
- 6.支持自定义拦截器的AOP扩展,可以处理参数、返回值、异常
快速使用
通用接入
- 1.maven(因域名问题,暂没有上传中央仓库),可以下载源码install到本地或者上传到私服,也可以先下载jar
<dependency>
<groupId>org.plume</groupId>
<artifactId>plumecache-core</artifactId>
<version>1.0.0</version>
</dependency>
复制代码
- 2.配置
plumecache:
instances:
- type: redis
endpoint: 127.0.0.1:6379
prefix: order
复制代码
- 3.使用
@Test
public void test(){
CacheService cacheService=CacheServiceFactory.getInstance();
cacheService.set("name","anson.yin");
System.out.println(cacheService.get("name"));
}
复制代码
spring boot接入
- 1.maven(因域名问题,暂没有上传中央仓库),可以下载源码install到本地或者上传到私服,也可以先下载jar
<dependency>
<groupId>org.plume</groupId>
<artifactId>plumecache-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
复制代码
- 2.配置
plumecache:
instances:
- type: redis
endpoint: 127.0.0.1:6379
prefix: order
复制代码
- 3.使用
@Autowired
private CacheService cacheService;
@RequestMapping(value = "/hello")
public String hello(){
cacheService.set("name","anson.yin");
return"hello,".concat(cacheService.get("name"));
}
复制代码
功能说明
配置
plumecache:
instances:
- type: redis #必填
name: redis #非必填,缺省为和type一样
endpoint: 127.0.0.1:6379 #必填
prefix: order #非必填,like "prefix@key"
serializer: org.plumecache.samples.FastjsonCacheSerializer #非必填,指定序列化类,缺省使用Gson序列化
compressor: org.plumecache.samples.NoneCacheCompressor #非必填,指定压缩类,缺省使用Gzip压缩
exclude-interceptors: SlowCacheInterceptor,LogCacheInterceptor #非必填,排除不需要的拦截器
复制代码
多实例
配置
plumecache:
instances:
- type: redis #必填
name: redis #非必填,缺省为和type一样
endpoint: 127.0.0.1:6379 #必填
prefix: order #非必填,like "prefix@key"
serializer: org.plumecache.samples.FastjsonCacheSerializer #非必填,指定序列化类,缺省使用Gson序列化
compressor: org.plumecache.samples.NoneCacheCompressor #非必填,指定压缩类,缺省使用Gzip压缩
exclude-interceptors: SlowCacheInterceptor,LogCacheInterceptor #非必填,排除不需要的拦截器
- type: memcached
endpoint: 127.0.0.1:11211
- type: memcached
name: memcached2
endpoint: 127.0.0.1:11222
- type: rediscluster
endpoint: 127.0.0.1:6379,127.0.0.1:6389,127.0.0.1:6399
复制代码
说明
- 0.上面的配置了四个缓存实例
- 1.默认的缓存实例是第一个配置
- 2.同一种类型多个实例,需要配置不同name来区分
代码示例
@Test
public void testMultipleInstances() {
//和CacheService redis =CacheServiceFactory.getInstance("redis")效果一样
CacheService redis = CacheServiceFactory.getInstance();
redis.set("instance", "redis");
System.out.println(redis.get("instance"));
CacheService memcached = CacheServiceFactory.getInstance("memcached");
memcached.set("instance", "memcached");
System.out.println(memcached.get("instance"));
}
复制代码
序列化
- 1.默认使用Gson作为序列化
- 2.自定义序列化,需要实现CacheSerializer接口,并且在配置配置序列化类
下面是FastjsonCacheSerializer的实现示例
public class FastjsonCacheSerializer implements CacheSerializer {
@Override
public <T> String serialize(T value) {
return JSON.toJSONString(value);
}
@Override
public <T> T deserialize(String value, Class<T> clazz) {
return JSON.parseObject(value, clazz);
}
}
复制代码
缓存压缩
- 1.实体类添加 @CacheCompress 注解
@Data
@CacheCompress
public class User {
private String name;
private Integer age;
private String address;
}
复制代码
- 2.调用方式不变
@Test
public void testCompress() {
User user = new User();
user.setAge(100);
user.setName("zhangsanfeng");
user.setAddress("zhangsanfengzhangsanfengzhangsanfengzhangsanfengzhangsanfengzhangsanfengzhangsanfengzhangsanfengzhangsanfengzhangsanfengzhangsanfeng");
cacheService.set("user", user);
User cacheUser = cacheService.get("user", User.class);
System.out.println(cacheUser);
}
复制代码
127.0.0.1:6379> get order@user
"\x04>]H4sIAAAAAAAAAKtWSkxJKUotLlayUqrKSMxLL07MS0vNSx8otpKOUmJ6qpKVoYGBjlJeYm4qmruU\nagHJDU9isgAAAA=="
复制代码
- 3.自定义压缩器
默认使用gzip压缩,也可以自定义实现,且添加配置
参考GzipCacheCompressor.java
缓存版本
- 1.实体类添加 @CacheVersion 注解
@Data
@CacheVersion("2.0")
public class User {
private String name;
private Integer age;
private String address;
}
复制代码
- 2.调用方式不变
@Test
public void testVersion() {
User user = new User();
user.setAge(100);
user.setName("zhangsanfeng");
user.setAddress("address");
cacheService.set("user", user);
User cacheUser = cacheService.get("user", User.class);
System.out.println(cacheUser);
}
复制代码
127.0.0.1:6379> get order@user@2.0
"\x04>5{\"name\":\"zhangsanfeng\",\"age\":100,\"address\":\"address\"}"
复制代码
拦截器
拦截器是对缓存操作的切面处理,自定义拦截器操作如下
- 1.实现CacheInterceptor,或者继承BaseCacheInterceptor,举例如下
@Slf4j
public class SlowCacheInterceptor extends BaseCacheInterceptor {
@Override
public boolean preHandle(CacheService target, Method method, Object[] args, Map<String, Object> context) {
Instant begin = Instant.now();
context.put("SlowCacheInstantBegin", begin);
return true;
}
@Override
public void postHandle(CacheService target, Method method, Object[] args, Map<String, Object> context, Object result) {
Instant begin = (Instant) context.get("SlowCacheInstantBegin");
if (Duration.between(begin, Instant.now()).toMillis() > 500) {
log.warn("[SlowCacheInterceptor]slow cache, method:{},args:{},result:{},cost:{}"
, method.getDeclaringClass().getName() + "." + method.getName()
, JSON.toJSONString(args)
, JSON.toJSONString(result)
, Duration.between(begin, Instant.now()).toMillis());
//do something others
}
}
}
复制代码
- 2.添加SPI的配置
path:resources/services/com.plumecache.core.interceptor.CacheInterceptor
org.plumecache.samples.SlowCacheInterceptor
复制代码
或者参考core包实现
- LogCacheInterceptor.java
- VersionCacheInterceptor.java
- PrefixCacheInterceptor.java
管理接口
在spring boot stater且为web项目时生效
- 1.list 显示实例列表
curl -X GET localhost:8080/plumecache/list
复制代码
[{"properties":{"name":"memcached","type":"memcached","endpoint":"127.0.0.1:11211","prefix":null,"serializer":null,"compressor":null,"excludeInterceptors":null},"statistics":{"cmd_touch":"0","moves_to_cold":"5","incr_hits":"1","get_flushed":"0","evictions":"0","touch_hits":"0","expired_unfetched":"0","pid":"1","time_in_listen_disabled_us":"0","response_obj_bytes":"65536","cas_badval":"0","cmd_flush":"0","total_items":"6","read_buf_oom":"0","round_robin_fallback":"0","slab_reassign_rescues":"0","log_watcher_skipped":"0","cas_hits":"0","accepting_conns":"1","auth_errors":"0","slab_reassign_evictions_nomem":"0","log_watcher_sent":"0","reserved_fds":"20","slab_reassign_running":"0","response_obj_oom":"0","crawler_items_checked":"12","direct_reclaims":"0","conn_yields":"0","slab_reassign_busy_deletes":"0","version":"1.6.10","read_buf_count":"8","listen_disabled_num":"0","slab_global_page_pool":"0","get_misses":"0","hash_is_expanding":"0","touch_misses":"0","get_expired":"0","auth_cmds":"0","cas_misses":"0","delete_misses":"0","cmd_meta":"0","get_hits":"4","slab_reassign_inline_reclaim":"0","malloc_fails":"0","delete_hits":"0","log_worker_written":"0","read_buf_bytes_free":"49152","lru_bumps_dropped":"0","curr_connections":"2","bytes_written":"4650","slab_reassign_busy_items":"0","hash_bytes":"524288","libevent":"2.1.8-stable","read_buf_bytes":"131072","lrutail_reflocked":"0","crawler_reclaimed":"0","decr_hits":"0","limit_maxbytes":"67108864","max_connections":"1024","decr_misses":"0","lru_crawler_running":"0","reclaimed":"0","rejected_connections":"0","cmd_get":"4","hash_power_level":"16","curr_items":"3","threads":"4","cmd_set":"5","bytes_read":"360","slab_reassign_chunk_rescues":"0","lru_crawler_starts":"27","uptime":"22253","log_worker_dropped":"0","unexpected_napi_ids":"0","total_connections":"9","evicted_active":"0","incr_misses":"1","connection_structures":"4","bytes":"221","lru_maintainer_juggles":"30247","evicted_unfetched":"0","rusage_system":"12.600134","time":"1638171110","slabs_moved":"0","moves_within_lru":"0","pointer_size":"64","moves_to_warm":"0","rusage_user":"8.090528","response_obj_count":"1"}},{"properties":{"name":"redis","type":"redis","endpoint":"127.0.0.1:6379","prefix":"order","serializer":"org.plumecache.samples.FastjsonCacheSerializer","compressor":"com.plumecache.core.compressor.NoneCacheCompressor","excludeInterceptors":["SlowCacheInterceptor","LogCacheInterceptor"]},"statistics":{"io_threaded_reads_processed":"0","tracking_clients":"0","uptime_in_seconds":"22224","cluster_connections":"0","current_cow_size":"0","maxmemory_human":"0B","aof_last_cow_size":"0","master_replid2":"0000000000000000000000000000000000000000","mem_replication_backlog":"0","aof_rewrite_scheduled":"0","total_net_input_bytes":"19507","rss_overhead_ratio":"1.58","hz":"10","current_cow_size_age":"0","redis_build_id":"69ab6eec4665acbc","aof_last_bgrewrite_status":"ok","multiplexing_api":"epoll","client_recent_max_output_buffer":"0","allocator_resident":"5181440","mem_fragmentation_bytes":"6549960","repl_backlog_first_byte_offset":"0","tracking_total_prefixes":"0","redis_mode":"standalone","cmdstat_get":"calls=10,usec=342,usec_per_call=34.20,rejected_calls=0,failed_calls=0","redis_git_dirty":"0","allocator_rss_bytes":"3031040","repl_backlog_histlen":"0","io_threads_active":"0","rss_overhead_bytes":"2990080","total_system_memory":"2082197504","loading":"0","evicted_keys":"0","maxclients":"10000","cmdstat_set":"calls=10,usec=1941,usec_per_call=194.10,rejected_calls=0,failed_calls=0","cluster_enabled":"0","redis_version":"6.2.5","repl_backlog_active":"0","mem_aof_buffer":"0","allocator_frag_bytes":"447160","io_threaded_writes_processed":"0","instantaneous_ops_per_sec":"0","used_memory_human":"1.59M","cmdstat_incr":"calls=1,usec=68,usec_per_call=68.00,rejected_calls=0,failed_calls=0","total_error_replies":"0","role":"master","maxmemory":"0","used_memory_lua":"37888","rdb_current_bgsave_time_sec":"-1","used_memory_startup":"809880","used_cpu_sys_main_thread":"146.683765","lazyfree_pending_objects":"0","used_memory_dataset_perc":"38.70%","allocator_frag_ratio":"1.26","arch_bits":"64","used_cpu_user_main_thread":"38.482678","mem_clients_normal":"512400","expired_time_cap_reached_count":"0","unexpected_error_replies":"0","mem_fragmentation_ratio":"5.04","aof_last_rewrite_time_sec":"-1","master_replid":"9c1aa9fc501b889e35727910956569cac1a2fda1","aof_rewrite_in_progress":"0","lru_clock":"10781132","maxmemory_policy":"noeviction","run_id":"d05aa7e4384c6ccd09ee83e919c9e9edcd2d8450","latest_fork_usec":"439","tracking_total_items":"0","total_commands_processed":"1349","expired_keys":"0","used_memory":"1664360","module_fork_in_progress":"0","dump_payload_sanitizations":"0","mem_clients_slaves":"0","keyspace_misses":"3","server_time_usec":"1638171084987972","executable":"/data/redis-server","lazyfreed_objects":"0","db0":"keys=232,expires=0,avg_ttl=0","used_memory_peak_human":"4.53M","keyspace_hits":"7","rdb_last_cow_size":"520192","used_memory_overhead":"1333640","active_defrag_hits":"0","tcp_port":"6379","uptime_in_days":"0","used_memory_peak_perc":"35.03%","current_save_keys_processed":"0","blocked_clients":"0","total_reads_processed":"1900","expire_cycle_cpu_milliseconds":"7066","sync_partial_err":"0","used_memory_scripts_human":"0B","aof_current_rewrite_time_sec":"-1","aof_enabled":"0","process_supervised":"no","cmdstat_info":"calls=2,usec=559,usec_per_call=279.50,rejected_calls=0,failed_calls=0","master_repl_offset":"0","used_memory_dataset":"330720","used_cpu_user":"38.497928","rdb_last_bgsave_status":"ok","tracking_total_keys":"0","cmdstat_ping":"calls=1325,usec=19580,usec_per_call=14.78,rejected_calls=0,failed_calls=0","atomicvar_api":"c11-builtin","allocator_rss_ratio":"2.41","client_recent_max_input_buffer":"16","clients_in_timeout_table":"0","aof_last_write_status":"ok","mem_allocator":"jemalloc-5.1.0","cmdstat_incrby":"calls=1,usec=21,usec_per_call=21.00,rejected_calls=0,failed_calls=0","used_memory_scripts":"0","used_memory_peak":"4751720","process_id":"1","master_failover_state":"no-failover","used_cpu_sys":"146.736539","repl_backlog_size":"1048576","connected_slaves":"0","current_save_keys_total":"0","gcc_version":"8.3.0","total_system_memory_human":"1.94G","sync_full":"0","connected_clients":"25","module_fork_last_cow_size":"0","total_writes_processed":"1349","allocator_active":"2150400","total_net_output_bytes":"18709","pubsub_channels":"0","current_fork_perc":"0.00","active_defrag_key_hits":"0","rdb_changes_since_last_save":"0","instantaneous_input_kbps":"0.00","configured_hz":"10","used_memory_rss_human":"7.79M","expired_stale_perc":"0.00","active_defrag_misses":"0","used_cpu_sys_children":"0.029570","number_of_cached_scripts":"0","sync_partial_ok":"0","used_memory_lua_human":"37.00K","rdb_last_save_time":"1638169845","pubsub_patterns":"0","slave_expires_tracked_keys":"0","redis_git_sha1":"00000000","used_memory_rss":"8171520","rdb_last_bgsave_time_sec":"0","os":"Linux 5.10.47-linuxkit x86_64","mem_not_counted_for_evict":"0","active_defrag_running":"0","rejected_connections":"0","total_forks":"3","active_defrag_key_misses":"0","allocator_allocated":"1703240","instantaneous_output_kbps":"0.00","second_repl_offset":"-1","rdb_bgsave_in_progress":"0","used_cpu_user_children":"0.012939","total_connections_received":"575","migrate_cached_sockets":"0"}}]
复制代码
- 2.execute 执行缓存操作
curl localhost:8080/plumecache/execute \
-X POST \
-H "Content-Type: application/json" \
-d '{"command":"get","key":"name"}'
复制代码
anson.yin
复制代码
接口说明
参考CacheService.java
RoadMap
version 1.0.0
- 1.支持spring boot 项目接入, 基于spring boot starter和 enable 以及 conditional注解完成初始化 --done
- 2.支持spring项目介入 提供初始化工具类 --done
- 3.支持key前缀功能 --done
- 4.支持多个cache实例 --done
- 5.支持get、set、delete、exists、expire、ttl命令 --done
- 6.支持dashboard api(实例列表,实例参数,命令执行)
- 7.支持自定义拦截器 --done
- 8.支持自定义序列化 --done
- 9.返回具体实例(redissonclient、MemcachedClient)对象 --done
- 10.实现hget,hset,hgetall,lock 命令 --done
- 11.支持缓存压缩功能(大value压缩) --done
- 12.支持版本功能(version 注解) --done
version 2.0.0
- 0.缓存参数配置(timeout,poolsize,idlecount,attempts)
- 1.支持线程内缓存(基于threadlocal)
- 2.支持字段序列化(基于ObjectOutputStream)
项目结构
作者:广陵笑笑生
链接:https://juejin.cn/post/7038850628965105672
本文暂时没有评论,来添加一个吧(●'◡'●)