app开发者平台在数字化时代的重要性与发展趋势解析
827
2022-10-21
基于数据库实现通用异步缓存系统
编写目的
在某些特殊的项目中,想实现缓存,但是不能使用中间件,存内存又会导致内存大幅度上升,怎么办呢?
降低预期,将需要缓存的数据存储在数据库,如何设计一套数据库缓存呢。
设计思路
一个KV形式缓存中间件需要有哪些基础功能?
1、增加缓存(新增数据库)2、缓存覆盖(修改数据库)3、缓存过期删除(删除数据库数据)4、查询缓存(查询数据库)
其实,就是对数据库的增删改查。但是缓存的数据一般情况是写入和查询比较频繁的。
查询优化: 在字段KEY上建立唯一索引插入优化:使用队列 + 定时任务异步入库缓存覆盖:使用队列 + 定时任务异步更新过期删除:使用队列 + 定时任务异步删除
表设计
主键,缓存KEY(唯一索引), 缓存值 , 过期时间
create table data_common_cache( cache_id bigint auto_increment comment '主键,生成序列号Id' primary key, cache_key varchar(100) not null comment '缓存的key 长度100 超过100的话通过编码后缩短', cache_value text default null comment '缓存的值', cache_expire datetime default null comment '过期时间', constraint udx_cache_key unique (cache_key)) comment '通用缓存表';
功能实现
使用SpringBoot + Mybatis Plus 实现通用缓存功能
依赖引入
配置文件
server: port: 8081spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/test_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true username: root password: root
生成entity, mapper
实体类
@Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)public class DataCommonCache implements Serializable { private static final long serialVersionUID=1L; /** * 主键,生成序列号Id */ @TableId(value = "cache_id", type = IdType.AUTO) private Long cacheId; /** * 缓存的key 长度100 超过100的话通过编码后缩短 */ private String cacheKey; /** * 缓存的值 */ private String cacheValue; /** * 过期时间 */ private Date cacheExpire;}
mapper, 包含自定义SQL
@Mapperpublic interface DataCommonCacheMapper extends BaseMapper
mapper.xml
整合MybatisPlus
整合MybatisPlus用于增删改差, 并实现了MybtaisPlus真正的批量新增和批量修改
数据源配置类DatasourceConfig
包含了数据源的配置和SqlSessionFactory配置,且注入了MybatisPlus的配置
/** * @Description 数据库相关配置 * @Author itdl * @Date 2022/08/10 09:20 */@Configuration@MapperScan(basePackages = "com.itdl.mapper", sqlSessionFactoryRef = "sqlSessionFactory")public class DatasourceConfig { @Bean(name = "dataSource") @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() throws SQLException { return DataSourceBuilder.create().build(); } @Bean("easySqlInjector") public EasySqlInjector easySqlInjector() { return new EasySqlInjector(); } @Bean public GlobalConfig globalConfig(EasySqlInjector easySqlInjector){ GlobalConfig globalConfig = new GlobalConfig(); globalConfig.setSqlInjector(easySqlInjector); return globalConfig; } @Bean(name = "sqlSessionFactory") public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource, GlobalConfig globalConfig) throws Exception { MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver(). getResources("classpath*:mapper/**/*.xml")); sessionFactoryBean.setPlugins(new PaginationInterceptor()); //添加自定义sql注入接口 sessionFactoryBean.setGlobalConfig(globalConfig);//添加自定义sql注入接口 return sessionFactoryBean.getObject(); }}
Mybatis 批量插入/更新配置
public class EasySqlInjector extends DefaultSqlInjector { @Override public List
缓存实现
实现思路
1、配置一个可调度的线程池,用于异步队列的调度2、编写一个基础调度父类,实现调度的基本逻辑3、编写缓存插入,覆盖,删除的调度逻辑4、将调度逻辑整合为一个缓存工具类5、使用Controller接口测试缓存增删改查
配置可调度的线程池
/** * @Description 通用配置及 * @Author itdl * @Date 2022/08/09 17:57 */@Configurationpublic class CommonConfig { @Bean("scheduledThreadPoolExecutor") public ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() { //线程名 String threadNameStr = "统一可调度线程-%d"; //线程工厂类就是将一个线程的执行单元包装成为一个线程对象,比如线程的名称,线程的优先级,线程是否是守护线程等线程; // guava为了我们方便的创建出一个ThreadFactory对象,我们可以使用ThreadFactoryBuilder对象自行创建一个线程. ThreadFactory threadNameVal = new ThreadFactoryBuilder().setNameFormat(threadNameStr).build(); // 单线程池 return new ScheduledThreadPoolExecutor( // 核心线程池 4, // 最大线程池 threadNameVal, // 使用策略为抛出异常 new ThreadPoolExecutor.AbortPolicy()); }}
编写可调度的公共父类,实现调度的基本逻辑
@Slf4jpublic abstract class BaseCacheHelper
增删改调度任务实现
@Slf4j@Componentpublic class DbCacheHelper{ @Slf4j @Component public static class InsertCache extends BaseCacheHelper
缓存工具类编写
在工具类里面实现缓存的逻辑
新增缓存思路
1、根据缓存key查询缓存ID和过期时间2、结果为空,表示没有缓存,发送数据到缓存队列,等待新增缓存队列任务调度3、结果不为空,继续判断过期时间,过期时间不为空,并且已经过期了,则发送到过期删除队列,等待调度4、没有过期,真正查询缓存的值, 比较值是是否更新,更新了发送更新数据到更新队列,没更新则不管
查询缓存思路
1、根据缓存key查询数据(包含缓存值)2、结果为空,表示缓存不存在,直接返回null3、结果不为空,判断是否过期,过期则发送过期删除到删除队列4、返回查询结果
删除缓存思路
1、根据缓存key查询数据(不包含缓存值)2、为空则不需要删除,不为空发送到删除队列,等待调度
代码实现
@Component@Slf4jpublic class DbCacheUtil { @Autowired private DataCommonCacheMapper dataCommonCacheMapper; @Autowired private DbCacheHelper.InsertCache insertCache; @Autowired private DbCacheHelper.UpdateCache updateCache; @Autowired private DbCacheHelper.DeleteCache deleteCache; /** * 插入缓存数据 * @param cacheKey 缓存key * @param cacheValue 缓存值 * @param ttl 单位毫秒 缓存失效时间 小于0表示永不过期 */ public synchronized void putCache(String cacheKey, String cacheValue, long ttl){ // 根据缓存key查询缓存 ID/过期时间等 final DataCommonCache cache = dataCommonCacheMapper.selectIdByKey(cacheKey); if (cache == null){ // 新增数据 DataCommonCache commonCache = buildInsertData(cacheKey, cacheValue, ttl); // 发送给入库队列 insertCache.producer(commonCache); return; } // 缓存设置了过期时间 并且缓存国企时间比当前时间小(过期了) if (cache.getCacheExpire() != null && cache.getCacheExpire().getTime() < System.currentTimeMillis()){ // 发送删除过期Key队列 deleteCache.producer(cacheKey); return; } // 都不是 表示需要覆盖缓存 也就是更新缓存 // 先判断缓存的值是否和数据库的值一致 一致则无需覆盖 final String cacheValueResult = dataCommonCacheMapper.selectValueByKey(cacheKey); if (StringUtils.equals(cacheValueResult, cacheValue)){ log.info("=============>>>>缓存key:{}的value与数据库一致,无需覆盖", cacheValue); return; } // 发送一个覆盖的请求 final DataCommonCache dataCommonCache = buildInsertData(cacheKey, cacheValue, ttl); dataCommonCache.setCacheId(cache.getCacheId()); updateCache.producer(dataCommonCache); } /** * 根据缓存从数据库查询 * @param cacheKey 缓存key * @return 缓存值cacheValue 这里返回的值可能是已过期的 知道过期key删除之后才会返回新的数据 */ public synchronized String getCache(String cacheKey){ // 根据缓存key查询缓存 ID/过期时间等 final DataCommonCache cache = dataCommonCacheMapper.selectByKey(cacheKey); if (cache == null){ log.info("===========缓存不存在, 请请先调用putCache缓存==========="); return null; } // 缓存设置了过期时间 并且缓存国企时间比当前时间小(过期了) if (cache.getCacheExpire() != null && cache.getCacheExpire().getTime() < System.currentTimeMillis()){ // 发送删除过期Key队列 deleteCache.producer(cacheKey); // 等待异步线程处理删除过期,但是这里还是返回缓存数据,从而减少数据库的压力,直到缓存删除后再次查询到结果在返回 } log.info("================命中缓存cacheKey为:{}=================", cacheKey); // 不为空,返回数据库 return cache.getCacheValue(); } /** * 根据key删除缓存 * @param cacheKey 缓存key */ public synchronized void deleteCache(String cacheKey){ // 根据缓存key查询缓存 ID/过期时间等 final DataCommonCache cache = dataCommonCacheMapper.selectIdByKey(cacheKey); if (cache == null){ log.info("===========缓存不存在 无需删除==========="); return; } // 发送删除消息到队列 deleteCache.producer(cacheKey); } private DataCommonCache buildInsertData(String cacheKey, String cacheValue, long ttl) { final DataCommonCache commonCache = new DataCommonCache(); commonCache.setCacheKey(cacheKey); commonCache.setCacheValue(cacheValue); // 失效时间为当前是时间 + ttl时间 Date expireTime = null; if (ttl > 0){ expireTime = new Date(System.currentTimeMillis() + ttl); } commonCache.setCacheExpire(expireTime); return commonCache; }}
缓存测试接口
@RestController@RequestMapping("/dbCache")public class DbCacheController { @Autowired private DbCacheUtil dbCacheUtil; /**缓存时间设置为5分钟, 可以自行设置*/ private static final Long ttl = 300 * 1000L; @GetMapping("/test/putCache") public String putCache(@RequestParam("cacheKey") String cacheKey, @RequestParam("cacheValue") String cacheValue){ dbCacheUtil.putCache(cacheKey, cacheValue, ttl); return "success"; } @GetMapping("/test/getCache") public String getCache(@RequestParam("cacheKey") String cacheKey){ return dbCacheUtil.getCache(cacheKey); } @GetMapping("/test/deleteCache") public String deleteCache(@RequestParam("cacheKey") String cacheKey){ dbCacheUtil.deleteCache(cacheKey); return "success"; }}
接口测试
新增接口:09:31:36.699 INFO 19572 --- [nio-8081-exec-2] com.itdl.cache.util.DbCacheUtil : ===========缓存不存在, 请请先调用putCache缓存===========2022-08-10 09:31:39.815 INFO 19572 --- [nio-8081-exec-3] com.itdl.cache.BaseCacheHelper : ================>>>通用队列:com.itdl.cache.DbCacheHelper$InsertCache:当前队列存在数据:12022-08-10 09:31:40.812 INFO 19572 --- [ 统一可调度线程-1] com.itdl.cache.BaseCacheHelper : =========>>>>消费数据1条2022-08-10 09:31:40.813 INFO 19572 --- [ 统一可调度线程-1] c.itdl.cache.DbCacheHelper$InsertCache : =====================开始插入缓存数据cacheKey:test_name===========================2022-08-10 09:31:40.851 INFO 19572 --- [ 统一可调度线程-1] c.itdl.cache.DbCacheHelper$InsertCache : =====================完成插入缓存数据cacheKey:test_name===========================2022-08-10 09:31:40.852 INFO 19572 --- [ 统一可调度线程-1] com.itdl.cache.BaseCacheHelper : =========>>>>通用队列:com.itdl.cache.DbCacheHelper$InsertCache:消费完成,总耗时:0.0383s<<<<=========2022-08-10 09:31:42.296 INFO 19572 --- [nio-8081-exec-4] com.itdl.cache.util.DbCacheUtil : ================命中缓存cacheKey为:test_name=================2022-08-10 10:18:51.256 INFO 19572 --- [nio-8081-exec-8] com.itdl.cache.BaseCacheHelper : ================>>>通用队列:com.itdl.cache.DbCacheHelper$DeleteCache:当前队列存在数据:12022-08-10 10:18:51.882 INFO 19572 --- [ 统一可调度线程-1] com.itdl.cache.BaseCacheHelper : =========>>>>消费数据1条2022-08-10 10:18:51.882 INFO 19572 --- [ 统一可调度线程-1] c.itdl.cache.DbCacheHelper$DeleteCache : =====================开始删除缓存数据cacheKey:test_name===========================2022-08-10 10:18:51.890 INFO 19572 --- [ 统一可调度线程-1] c.itdl.cache.DbCacheHelper$DeleteCache : =====================完成删除写入缓存数据cacheKey:test_name===========================2022-08-10 10:18:51.890 INFO 19572 --- [ 统一可调度线程-1] com.itdl.cache.BaseCacheHelper : =========>>>>通用队列:com.itdl.cache.DbCacheHelper$DeleteCache:消费完成,总耗时:0.0079s<<<<=========2022-08-10 10:19:01.817 INFO 19572 --- [nio-8081-exec-9] com.itdl.cache.util.DbCacheUtil : ===========缓存不存在, 请请先调用putCache缓存===========
项目地址
https://github.com/HedongLin123/db-cache-demo
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~