需求描述
由于项目分为多个模块,每个模块使用同一个redis的不同DB,有时需要在不同的模块使用相同的缓存,一开始想到的策略是通过模块间通讯的方式,将需要同步的缓存内容发送到对应模块,有相应的模块进行处理;但是这样的的方式不太面向对象,于是考虑使用同一模块切换DB进行操作。
查找解决方案
说明:目前只针对手动切换,没有做多DB自动切换策略。自动切换DB后期实践后在进行补充
网上找了很多资料,这方面的资料也很多,解决方案都基本一致:
获取到RedisConnectionFactory,在factory中设置DB,然后重连实现切换DB,实现逻辑如下:
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testUpdateDB() {
LettuceConnectionFactory connectionFactory = (LettuceConnectionFactory ) redisTemplate.getConnectionFactory();
int database = connectionFactory .getDatabase();
log.info("curr redis template db: {}", database);
log.info("set cf before keys: \n{}", redisTemplate.keys("*"));
connectionFactory.setDatabase(7);
redisTemplate.setConnectionFactory(connectionFactory);
connectionFactory.resetConnection();
log.info("changed redis template db: {}", cf1.getDatabase());
log.info("set cf after keys: \n{}", redisTemplate.keys("*"));
}
但结果并不理想,切换前后输出的DB确实变了,但是查询到的key值还是切换前的,也就是切换后其实并没重连接。
网上找了很多资料,说是这样做只有某个版本的才可以这样操作,其他的切换不了。尝试各种办法,比如:
redisTemplate.setConnectionFactory(connectionFactory);
方法后调用redisTemplate.afterPropertiesSet();
依然无效。于是有继续找资料。最终看到一篇博客(喝水不忘挖井人),解决了不重连的问题:
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testUpdateDB() {
LettuceConnectionFactory connectionFactory = (LettuceConnectionFactory ) redisTemplate.getConnectionFactory();
int database = connectionFactory .getDatabase();
log.info("curr redis template db: {}", database);
log.info("set cf before keys: \n{}", redisTemplate.keys("*"));
connectionFactory.setDatabase(7);
connectionFactory.afterPropertiesSet(); // TODO 注意这行代码的加入
cconnectionFactory.resetConnection();
log.info("changed redis template db: {}", cf1.getDatabase());
log.info("set cf after keys: \n{}", redisTemplate.keys("*"));
}
于是找到了问题原因:需要调用connectionFactory.afterPropertiesSet();
方法手动触发factory刷新重新初始化后才能真正切换到新的DB。
一个优化小技巧
真实的业务场景,其实是通用redis+数据库实现序列编号的自动计算:每次申请序列编号时,不确保一定被使用,于是使用数据库持久化记录当前被使用的最后一个编号,然后将未使用的序列编号存入redis,下次申请时,先看redis中是否有编号,有则直接返回缓存的编号,当申请者确认使用该编号后移除缓存;没有创建新的编号,并存入reids。
但是这样有个问题,使用编号者不讲武德,没使用掉他也进行了确认使用的操作,导致需要回滚编号。但是回滚编号需要将数据库和redis缓存都进行处理,但是项目不运行以任何方式(客户端或者命令行方式)进行redis操作,于是只能通过在程序中通过接口的方式进行处理。
但是考虑到项目有多个模块,都分别使用了不同的DB,并且其他模块没有对外的GUI界面,于是考虑在当前模块中去实现跨DB进行处理。于是得考虑 操作完后需要将DB恢复到切换前的DB上。
上代码
@RestController
@RequestMapping("/monitor/cache")
public class CacheController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/** 切换redis数据库,并返回切换前的DB */
private Integer changeDb(Integer dbIndex) {
if (dbIndex == null) return null;
LettuceConnectionFactory connectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory();
if (connectionFactory == null) {
throw new RuntimeException("redis未连接到服务器");
}
Integer orgDB = connectionFactory.getDatabase();
if (!orgDB.equals(dbIndex)) {
connectionFactory.setDatabase(dbIndex);
connectionFactory.afterPropertiesSet();
connectionFactory.resetConnection();
}
return orgDB;
}
/** 查询指定DB下的所有key */
@GetMapping("/all/keys")
public AjaxResult getKeys(String pattern, Integer dbIndex) {
Integer orgDB = changeDb(dbIndex);// 切换到指定DB,并记录原DB
if (ObjectUtils.isEmpty(pattern)) {
pattern = "*";
}
Set<String> keys = redisTemplate.keys(pattern);
changeDb(orgDB);// 操作结束,切换会原DB
return AjaxResult.success(new TreeSet<>(keys == null ? new ArrayList<>() : keys));
}
/** 删除指定DB中的指定键值对 */
@PostMapping("/del/key")
public AjaxResult getDbKeys(@RequestBody CacheRemoveDTO dto) {
Integer orgDB = changeDb(dto.getDbIndex());
Long del = redisTemplate.delete(dto.getKey());
changeDb(orgDB);
return AjaxResult.success(del);
}
CacheRemoveDTO 代码如下:
/**
* redis缓存删除DTO
*
* @author pengyl
* @date 2024/7/1 14:56
* @description redis缓存删除DTO
*/
@Getter
@Setter
public class CacheRemoveDTO {
@NotEmpty(message = "缓存key不能为空")
private List<String> key; // 一次可删除多个Key
private Integer dbIndex;
}