文章目录

  • 1. 数据库表
  • 2. 更新MySQL数据并删除Redis缓存
  • 3. 查询缓存中数据
  • 4. 删除数据库中数据并删除缓存
  • 5. 生成redis的key
  • 6. 常量类 StorageConstants
  • 7. 配置类 StorageConfig
  • 8. 工具类 JsonStorage
  • 9. 应用测试


1. 数据库表

CREATE TABLE `t_storage` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `nameSpace` varchar(64) NOT NULL COMMENT '隔离字段',
  `groupId` varchar(128) NOT NULL COMMENT '分组,比如不同app',
  `dataId` varchar(64) NOT NULL COMMENT '数据存储id',
  `dataValue` mediumtext DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `nameSpace` (`nameSpace`,`groupId`,`dataId`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

向表中插入数据:

INSERT INTO `t_storage`(`id`, `nameSpace`, `groupId`, `dataId`, `dataValue`) VALUES (10, '_system', 'logsetting', 'thresholdConfig', '{\"threshold\":82,\"allSize\":\"47G\",\"usedSize\":\"0\",\"remainSize\":\"47G\",\"usingRate\":0,\"diskName\":null}');
INSERT INTO `t_storage`(`id`, `nameSpace`, `groupId`, `dataId`, `dataValue`) VALUES (34, '_system', 'logsetting', 'lruConfigFilepath', '{\"dsMongo\":null,\"dsFile\":null,\"dsEs\":null}');
INSERT INTO `t_storage`(`id`, `nameSpace`, `groupId`, `dataId`, `dataValue`) VALUES (45, '_system', 'auth', 'policy', '{\"passwordPeriod\":90,\"errorTimes\":5,\"lockTime\":5,\"minLength\":8}');

可以看到dataValue的值为对象序列化后的字符串;

2. 更新MySQL数据并删除Redis缓存

先更新数据库再删除缓存

@Service
@Slf4j
public class StorageDb implements IStorage {
    
    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private StorageConfig storageConfig;

    @Override
    @CheckParam
    public void setDataValue(String nameSpace,String groupId,String dataId,String value
    ) throws StorageException {
        // 同一nameSpace, groupId, dataId下数据不存在则新增,否则更新
        String updateSql = String.format(
            "insert into %s(nameSpace,groupId,dataId,dataValue) values (?,?,?,?) "
            + " on duplicate key update dataValue=values(dataValue)",
            StorageConstants.TABLE_NAME
        );
        // 更新数据库表
        int rows = jdbcTemplate.update(updateSql, nameSpace, groupId, dataId, value);
        if (rows > 0) {
            // 拼接redis的key
            String keyName = generateKey(nameSpace, groupId, dataId);
            // 更新完数据将Redis缓存删除
            redisUtils.delete(keyName);
        }
    }
}

3. 查询缓存中数据

先从redis缓存中查询,当缓存中查询不到时,再查询数据库,查完数据库将数据设置到redis缓存,如果查询中查询的数据为null也要缓存,防止缓存穿透

@Service
@Slf4j
public class StorageDb implements IStorage {

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private StorageConfig storageConfig;
    @Override
    @CheckParam
    public String getDataValue( String nameSpace,String groupId,String dataId) {
        // 拼接redis缓存key
        String keyName = generateKey(nameSpace, groupId, dataId);
        // 先从缓存中查询数据
        String dataValue = redisUtils.getString(keyName);
        // 缓存不存在则查询数据库
        if (Objects.isNull(dataValue)) {
            String sql = String.format("select dataValue from %s where nameSpace=? and groupId=? and dataId=?", StorageConstants.TABLE_NAME);
            try {
                dataValue = jdbcTemplate.queryForObject(sql, String.class, nameSpace, groupId, dataId);
            } catch (DataAccessException e) {
                log.error("Failed to query storage, nameSpace = {}, groupId = {}, dataId = {}, error {}", nameSpace, groupId, dataId, e);
            }
            // 查询完数据将其写入redis缓存,空值也缓存,防止缓存穿透
            redisUtils.setWithExpired(keyName, StringUtils.isEmpty(dataValue) ? "" : dataValue, storageConfig.getExpiredTime(), TimeUnit.SECONDS);
        }
        // 只返回dataValue
        return dataValue;
    }
}

4. 删除数据库中数据并删除缓存

当数据库中数据删除成功后,删除缓存中的数据

@Service
@Slf4j
public class StorageDb implements IStorage {

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private StorageConfig storageConfig;    
    @Override
    @CheckParam
    public void deleteData(String nameSpace,String groupId,String dataId
    ) {
        String sql = String.format("delete from %s where nameSpace=? and groupId=? and dataId=?", StorageConstants.TABLE_NAME);
        int rows = jdbcTemplate.update(sql, nameSpace, groupId, dataId);
        // 数据库删除成功则删掉redis缓存中的数据
        if (rows > 0) {
            redisUtils.delete(generateKey(nameSpace, groupId, dataId));
        }
    }
}

5. 生成redis的key

@Service
@Slf4j
public class StorageDb implements IStorage { 
   /**
    * 生成rediskey
    */
    private String generateKey(String... fields) {
        return StorageConstants.REDIS_KEY_PREFIX
            .concat(String.join(StorageConstants.SEPARATOR, fields));
    }
}

6. 常量类 StorageConstants

public interface StorageConstants {
    /**
     * 数据表名
     */
    String TABLE_NAME = "t_storage";
    /**
     * redis命名前缀
     */
    String REDIS_KEY_PREFIX = "ngsoc:storage";
    /**
     * redis key分隔符
     */
    String SEPARATOR = ":";
    /**
     * redis数据过期时间
     */
    long EXPIRED_TIME = 30;
}

7. 配置类 StorageConfig

@Data
@Configuration
@ConfigurationProperties(prefix = "storage.storage")
public class StorageConfig {
    /**
     * Redis过期时间
     */
    private Integer expiredTime;
}
storage:
  storage:
    expired_time: 30  #redis过期时间
    batch_save_size: 5000  #批量插入大小

8. 工具类 JsonStorage

因为dataValue的值为对象序列化之后的字符串,因此需要写一个工具类,在参数为对象时将其序列化为json字符串存入数据库表,在从数据库表中取出数据时将dataValue反序列化为对象;

@Service
@Slf4j
public class JsonStorage {

    private IStorage storage;

    @Autowired
    private void setStorage(IStorage storage) {
        this.storage = storage;
    }

    private final ObjectMapper JSON_MAPPER = new ObjectMapper();

    /**
     * 通过实体对象设置Json字符串
     */
    public boolean setJsonDataWithEntity(String nameSpace, String groupId, String key, Object entity) {
        try {
            // 将对象序列化为json字符串,并存入数据库和redis缓存中
            String jsonString = JSON_MAPPER.writeValueAsString(entity);
            storage.setDataValue(nameSpace, groupId, key, jsonString);
            return true;
        } catch (JsonProcessingException | StorageException e) {
            log.error("Storage setValue failed, error: {}", e);
            return false;
        }
    }

    /**
     * 获取由JsonString转化后的实体对象
     */
    public <T> T getJsonDataAsEntity(String nameSpace, String groupId, String key, Class<T> classType) throws StorageException {
        try {
            // 获取dataValue
            String jsonString = storage.getDataValue(nameSpace, groupId, key);
            if (!StringUtils.isEmpty(jsonString)) {
                // 不匹配字段忽略
                JSON_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                // 将dataValue反序列化为实体对象并返回,用泛型T接收
                return JSON_MAPPER.readValue(jsonString, classType);
            } else {
                log.error("Storage get value failed, value is empty");
                throw new StorageException("storage.value.empty");
            }
        } catch (JsonProcessingException e) {
            log.error("Storage get value failed, jsonString format error", e);
            throw new StorageException("storage.getValue.format.error");
        }
    }
}

9. 应用测试

密码策略实体类 PasswordPolicyEntity

@ApiModel
@Data
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class PasswordPolicyEntity {

    /**
     * 密码周期
     */
    @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS)
    @ApiModelProperty(value = "密码周期", required = true)
    private Integer passwordPeriod = 90;

    /**
     * 登录密码输错次数限制
     */
    @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS)
    @ApiModelProperty(value = "登录密码输错次数限制", required = true)
    private Integer errorTimes = 5;

    /**
     * 密码输入错误达到限制后锁定锁定时间
     */
    @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS)
    @ApiModelProperty(value = "密码输入错误达到限制后锁定锁定时间", required = true)
    private Integer lockTime = 5;

    /**
     * 密码最小长度
     */
    @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS)
    @ApiModelProperty(value = "密码最小长度", required = true)
    private Integer minLength = 8;
}

读取密码策略 StorageApplicationTest

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class StorageApplicationTest {

    @Autowired
    private JsonStorage jsonStorage;

    private final ReentrantReadWriteLock.ReadLock readLock = new ReentrantReadWriteLock().readLock();

    private final ReentrantReadWriteLock.WriteLock writeLock = new ReentrantReadWriteLock().writeLock();

    @Test
    public void test() throws StorageException {
        PasswordPolicyEntity passwordPolicyEntity = null;
        readLock.lock();
        try {
            passwordPolicyEntity = jsonStorage.getJsonDataAsEntity(
                    "_system", "auth", "policy", PasswordPolicyEntity.class);
        } catch (StorageException e) {
            log.error("Error read pwd policy", e);
        } finally {
            readLock.unlock();
        }
        System.out.println("passwordPolicyEntity = " + passwordPolicyEntity);
    }
}