|
|
@@ -0,0 +1,491 @@
|
|
|
+package com.ruoyi.common.core.cache;
|
|
|
+
|
|
|
+import com.ruoyi.common.utils.StringUtils;
|
|
|
+import com.ruoyi.common.utils.JacksonUtils;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.dao.EmptyResultDataAccessException;
|
|
|
+import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import java.sql.Timestamp;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 数据库缓存实现
|
|
|
+ * 使用sys_cache表存储缓存数据
|
|
|
+ *
|
|
|
+ * @author ruoyi
|
|
|
+ */
|
|
|
+@Component
|
|
|
+public class DbCache {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(DbCache.class);
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private JdbcTemplate jdbcTemplate;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 缓存基本的对象,Integer、String、实体类等
|
|
|
+ *
|
|
|
+ * @param key 缓存的键值
|
|
|
+ * @param value 缓存的值
|
|
|
+ */
|
|
|
+ public <T> void setCacheObject(final String key, final T value) {
|
|
|
+ setCacheObject(key, value, null, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 缓存基本的对象,Integer、String、实体类等
|
|
|
+ *
|
|
|
+ * @param key 缓存的键值
|
|
|
+ * @param value 缓存的值
|
|
|
+ * @param timeout 时间
|
|
|
+ * @param timeUnit 时间颗粒度
|
|
|
+ */
|
|
|
+ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
|
|
|
+ if (StringUtils.isAnyBlank(key) || value == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String cacheValue = JacksonUtils.toJSONString(value);
|
|
|
+ Timestamp expiryTime = calculateExpiryTime(timeout, timeUnit);
|
|
|
+
|
|
|
+ // 使用INSERT ... ON CONFLICT语句实现插入或更新(PostgreSQL兼容)
|
|
|
+ String sql = "INSERT INTO sys_cache (cache_key, cache_value, expiry_time, create_time, update_time) " +
|
|
|
+ "VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " +
|
|
|
+ "ON CONFLICT (cache_key) DO UPDATE SET " +
|
|
|
+ "cache_value = EXCLUDED.cache_value, expiry_time = EXCLUDED.expiry_time, update_time = CURRENT_TIMESTAMP";
|
|
|
+
|
|
|
+ jdbcTemplate.update(sql, key, cacheValue, expiryTime);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("设置缓存失败 key: {}", key, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置有效时间(默认时间单位为秒)
|
|
|
+ *
|
|
|
+ * @param key Redis键
|
|
|
+ * @param timeout 超时时间(秒)
|
|
|
+ * @return true=设置成功;false=设置失败
|
|
|
+ */
|
|
|
+ public boolean expire(final String key, final long timeout) {
|
|
|
+ return expire(key, timeout, TimeUnit.SECONDS);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置有效时间
|
|
|
+ *
|
|
|
+ * @param key Redis键
|
|
|
+ * @param timeout 超时时间
|
|
|
+ * @param unit 时间单位
|
|
|
+ * @return true=设置成功;false=设置失败
|
|
|
+ */
|
|
|
+ public boolean expire(final String key, final long timeout, final TimeUnit unit) {
|
|
|
+ if (StringUtils.isBlank(key)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ Timestamp expiryTime = calculateExpiryTime((int) timeout, unit);
|
|
|
+ String sql = "UPDATE sys_cache SET expiry_time = ?, update_time = CURRENT_TIMESTAMP WHERE cache_key = ?";
|
|
|
+ int affected = jdbcTemplate.update(sql, expiryTime, key);
|
|
|
+ return affected > 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("设置缓存过期时间失败 key: {}", key, e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获得缓存的基本对象。
|
|
|
+ *
|
|
|
+ * @param key 缓存键值
|
|
|
+ * @return 缓存键值对应的数据
|
|
|
+ */
|
|
|
+ public Object getCacheObject(final String key) {
|
|
|
+ if (StringUtils.isBlank(key)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 先清理过期缓存
|
|
|
+ cleanupExpiredCache();
|
|
|
+
|
|
|
+ String sql = "SELECT cache_value FROM sys_cache WHERE cache_key = ? AND expiry_time > CURRENT_TIMESTAMP";
|
|
|
+ String cacheValue = jdbcTemplate.queryForObject(sql, String.class, key);
|
|
|
+
|
|
|
+ if (StringUtils.isNotBlank(cacheValue)) {
|
|
|
+ // 由于数据库缓存存储的是JSON字符串,我们无法知道具体类型
|
|
|
+ // 返回Object类型,由调用方进行类型转换
|
|
|
+ return cacheValue;
|
|
|
+ }
|
|
|
+ } catch (EmptyResultDataAccessException e) {
|
|
|
+ // 缓存不存在,返回null
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取缓存失败 key: {}", key, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获得缓存的基本对象(类型安全版本)
|
|
|
+ *
|
|
|
+ * @param key 缓存键值
|
|
|
+ * @param clazz 对象类型
|
|
|
+ * @return 缓存键值对应的数据
|
|
|
+ */
|
|
|
+ public <T> T getCacheObject(final String key, Class<T> clazz) {
|
|
|
+ if (StringUtils.isBlank(key)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 先清理过期缓存
|
|
|
+ cleanupExpiredCache();
|
|
|
+
|
|
|
+ String sql = "SELECT cache_value FROM sys_cache WHERE cache_key = ? AND expiry_time > CURRENT_TIMESTAMP";
|
|
|
+ String cacheValue = jdbcTemplate.queryForObject(sql, String.class, key);
|
|
|
+
|
|
|
+ if (StringUtils.isNotBlank(cacheValue)) {
|
|
|
+ // 解析JSON字符串为指定类型
|
|
|
+ return JacksonUtils.parseObject(cacheValue, clazz);
|
|
|
+ }
|
|
|
+ } catch (EmptyResultDataAccessException e) {
|
|
|
+ // 缓存不存在,返回null
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取缓存失败 key: {}", key, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获得缓存的基本对象(支持泛型类型)
|
|
|
+ *
|
|
|
+ * @param key 缓存键值
|
|
|
+ * @param typeReference 类型引用
|
|
|
+ * @return 缓存键值对应的数据
|
|
|
+ */
|
|
|
+ public <T> T getCacheObject(final String key, com.fasterxml.jackson.core.type.TypeReference<T> typeReference) {
|
|
|
+ if (StringUtils.isBlank(key)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 先清理过期缓存
|
|
|
+ cleanupExpiredCache();
|
|
|
+
|
|
|
+ String sql = "SELECT cache_value FROM sys_cache WHERE cache_key = ? AND expiry_time > CURRENT_TIMESTAMP";
|
|
|
+ String cacheValue = jdbcTemplate.queryForObject(sql, String.class, key);
|
|
|
+
|
|
|
+ if (StringUtils.isNotBlank(cacheValue)) {
|
|
|
+ // 解析JSON字符串为指定类型
|
|
|
+ return JacksonUtils.parseObject(cacheValue, typeReference);
|
|
|
+ }
|
|
|
+ } catch (EmptyResultDataAccessException e) {
|
|
|
+ // 缓存不存在,返回null
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取缓存失败 key: {}", key, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除单个对象
|
|
|
+ *
|
|
|
+ * @param key
|
|
|
+ */
|
|
|
+ public boolean deleteObject(final String key) {
|
|
|
+ if (StringUtils.isBlank(key)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String sql = "DELETE FROM sys_cache WHERE cache_key = ?";
|
|
|
+ int affected = jdbcTemplate.update(sql, key);
|
|
|
+ return affected > 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("删除缓存失败 key: {}", key, e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除集合对象
|
|
|
+ *
|
|
|
+ * @param collection 多个对象
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ public boolean deleteObject(final Collection collection) {
|
|
|
+ if (collection == null || collection.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String sql = "DELETE FROM sys_cache WHERE cache_key IN (" +
|
|
|
+ StringUtils.join(collection, ",") + ")";
|
|
|
+ int affected = jdbcTemplate.update(sql);
|
|
|
+ return affected > 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("批量删除缓存失败", e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 缓存List数据
|
|
|
+ *
|
|
|
+ * @param key 缓存的键值
|
|
|
+ * @param dataList 待缓存的List数据
|
|
|
+ * @return 缓存的对象
|
|
|
+ */
|
|
|
+ public <T> long setCacheList(final String key, final List<T> dataList) {
|
|
|
+ setCacheObject(key, dataList);
|
|
|
+ return dataList != null ? dataList.size() : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获得缓存的list对象
|
|
|
+ *
|
|
|
+ * @param key 缓存的键值
|
|
|
+ * @return 缓存键值对应的数据
|
|
|
+ */
|
|
|
+ public <T> List<T> getCacheList(final String key) {
|
|
|
+ Object result = getCacheObject(key);
|
|
|
+ if (result instanceof String) {
|
|
|
+ // 解析JSON字符串为List
|
|
|
+ return JacksonUtils.parseObject((String) result, List.class);
|
|
|
+ }
|
|
|
+ return (List<T>) result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 缓存Set
|
|
|
+ *
|
|
|
+ * @param key 缓存键值
|
|
|
+ * @param dataSet 缓存的数据
|
|
|
+ * @return 缓存数据的对象
|
|
|
+ */
|
|
|
+ public <T> long setCacheSet(final String key, final Set<T> dataSet) {
|
|
|
+ setCacheObject(key, dataSet);
|
|
|
+ return dataSet != null ? dataSet.size() : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获得缓存的set
|
|
|
+ *
|
|
|
+ * @param key
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ public <T> Set<T> getCacheSet(final String key) {
|
|
|
+ Object result = getCacheObject(key);
|
|
|
+ if (result instanceof String) {
|
|
|
+ // 解析JSON字符串为Set
|
|
|
+ return JacksonUtils.parseObject((String) result, Set.class);
|
|
|
+ }
|
|
|
+ return (Set<T>) result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 缓存Map
|
|
|
+ *
|
|
|
+ * @param key
|
|
|
+ * @param dataMap
|
|
|
+ */
|
|
|
+ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
|
|
|
+ setCacheObject(key, dataMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获得缓存的Map
|
|
|
+ *
|
|
|
+ * @param key
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ public <T> Map<String, T> getCacheMap(final String key) {
|
|
|
+ Object result = getCacheObject(key);
|
|
|
+ if (result instanceof String) {
|
|
|
+ // 解析JSON字符串为Map
|
|
|
+ return JacksonUtils.parseObject((String) result, Map.class);
|
|
|
+ }
|
|
|
+ return (Map<String, T>) result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 往Hash中存入数据
|
|
|
+ *
|
|
|
+ * @param key Redis键
|
|
|
+ * @param hKey Hash键
|
|
|
+ * @param value 值
|
|
|
+ */
|
|
|
+ public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
|
|
|
+ // 数据库缓存不支持Hash结构,使用普通缓存
|
|
|
+ setCacheObject(key + ":" + hKey, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取Hash中的数据
|
|
|
+ *
|
|
|
+ * @param key Redis键
|
|
|
+ * @param hKey Hash键
|
|
|
+ * @return Hash中的对象
|
|
|
+ */
|
|
|
+ public <T> T getCacheMapValue(final String key, final String hKey) {
|
|
|
+ Object result = getCacheObject(key + ":" + hKey);
|
|
|
+ if (result instanceof String) {
|
|
|
+ // 由于数据库缓存存储的是JSON字符串,我们无法知道具体类型
|
|
|
+ // 返回Object类型,由调用方进行类型转换
|
|
|
+ return (T) result;
|
|
|
+ }
|
|
|
+ return (T) result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除Hash中的数据
|
|
|
+ *
|
|
|
+ * @param key
|
|
|
+ * @param hkey
|
|
|
+ */
|
|
|
+ public void delCacheMapValue(final String key, final String hkey) {
|
|
|
+ deleteObject(key + ":" + hkey);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取多个Hash中的数据
|
|
|
+ *
|
|
|
+ * @param key Redis键
|
|
|
+ * @param hKeys Hash键集合
|
|
|
+ * @return Hash对象集合
|
|
|
+ */
|
|
|
+ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
|
|
|
+ List<T> result = new ArrayList<>();
|
|
|
+ for (Object hKey : hKeys) {
|
|
|
+ T value = getCacheMapValue(key, hKey.toString());
|
|
|
+ if (value != null) {
|
|
|
+ result.add(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获得缓存的基本对象列表
|
|
|
+ *
|
|
|
+ * @param pattern 字符串前缀
|
|
|
+ * @return 对象列表
|
|
|
+ */
|
|
|
+ public Collection<String> keys(final String pattern) {
|
|
|
+ try {
|
|
|
+ // 清理过期缓存
|
|
|
+ cleanupExpiredCache();
|
|
|
+
|
|
|
+ String sql = "SELECT cache_key FROM sys_cache WHERE cache_key LIKE ? AND expiry_time > CURRENT_TIMESTAMP";
|
|
|
+ return jdbcTemplate.queryForList(sql, String.class, pattern.replace("*", "%"));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("查询缓存键失败 pattern: {}", pattern, e);
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取缓存的有效期
|
|
|
+ *
|
|
|
+ * @param key Redis键
|
|
|
+ * @return 有效期(秒)
|
|
|
+ */
|
|
|
+ public long getExpire(final String key) {
|
|
|
+ if (StringUtils.isBlank(key)) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String sql = "SELECT expiry_time FROM sys_cache WHERE cache_key = ?";
|
|
|
+ Timestamp expiryTime = jdbcTemplate.queryForObject(sql, Timestamp.class, key);
|
|
|
+
|
|
|
+ if (expiryTime != null) {
|
|
|
+ long remaining = expiryTime.getTime() - System.currentTimeMillis();
|
|
|
+ return remaining > 0 ? remaining / 1000 : -1;
|
|
|
+ }
|
|
|
+ } catch (EmptyResultDataAccessException e) {
|
|
|
+ // 缓存不存在
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取缓存有效期失败 key: {}", key, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断key是否存在
|
|
|
+ *
|
|
|
+ * @param key 键
|
|
|
+ * @return true 存在 false不存在
|
|
|
+ */
|
|
|
+ public boolean hasKey(final String key) {
|
|
|
+ if (StringUtils.isBlank(key)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 清理过期缓存
|
|
|
+ cleanupExpiredCache();
|
|
|
+
|
|
|
+ String sql = "SELECT COUNT(*) FROM sys_cache WHERE cache_key = ? AND expiry_time > CURRENT_TIMESTAMP";
|
|
|
+ Integer count = jdbcTemplate.queryForObject(sql, Integer.class, key);
|
|
|
+ return count != null && count > 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("检查缓存存在性失败 key: {}", key, e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清理过期缓存
|
|
|
+ */
|
|
|
+ private void cleanupExpiredCache() {
|
|
|
+ try {
|
|
|
+ String sql = "DELETE FROM sys_cache WHERE expiry_time <= CURRENT_TIMESTAMP";
|
|
|
+ jdbcTemplate.update(sql);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("清理过期缓存失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算过期时间
|
|
|
+ */
|
|
|
+ private Timestamp calculateExpiryTime(Integer timeout, TimeUnit timeUnit) {
|
|
|
+ if (timeout == null || timeUnit == null) {
|
|
|
+ // 默认30天过期
|
|
|
+ return Timestamp.valueOf(LocalDateTime.now().plusDays(30));
|
|
|
+ }
|
|
|
+
|
|
|
+ LocalDateTime expiryTime = LocalDateTime.now();
|
|
|
+ switch (timeUnit) {
|
|
|
+ case SECONDS:
|
|
|
+ expiryTime = expiryTime.plusSeconds(timeout);
|
|
|
+ break;
|
|
|
+ case MINUTES:
|
|
|
+ expiryTime = expiryTime.plusMinutes(timeout);
|
|
|
+ break;
|
|
|
+ case HOURS:
|
|
|
+ expiryTime = expiryTime.plusHours(timeout);
|
|
|
+ break;
|
|
|
+ case DAYS:
|
|
|
+ expiryTime = expiryTime.plusDays(timeout);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ expiryTime = expiryTime.plusDays(30);
|
|
|
+ }
|
|
|
+
|
|
|
+ return Timestamp.valueOf(expiryTime);
|
|
|
+ }
|
|
|
+}
|