需求背景:第三方接口返回数据量太大,直接返回给前端容易导致页面崩溃,还不想存入数据库中,那么缓存是一个不错的选择,redis,es,都可以,此处尝试使用redis中的scan命令实现数据的模糊匹配搜索;
1、导入redisson依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.7</version>
</dependency>
2、注入spring
@Configuration
public class RedisConfig {
@Value("${spring.redis.host:127.0.0.1}")
private String host;
@Value("${spring.redis.port:6379}")
private int port;
@Value("${spring.redis.timeout:0}")
private int timeout;
@Value("${spring.redis.password:#{null}}")
private String password;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.setTransportMode(TransportMode.NIO);
config.useSingleServer()
.setAddress("redis://" + host + ":" + port)
.setPassword(password)
.setTimeout(timeout)
// 重试次数3
.setRetryAttempts(3)
// 失败重试间隔1s
.setRetryInterval(1000)
// 心跳检测间隔1s
.setPingConnectionInterval(1000);
config.setCodec(new JsonJacksonCodec());
return Redisson.create(config);
}
/**
* 创建redisson缓存工厂
*
* @param client
* @return
*/
@Bean(name = "redissonConnectionFactory")
public RedissonConnectionFactory getFactory(RedissonClient client) {
var redissonConnectionFactory = new RedissonConnectionFactory(client);
return redissonConnectionFactory;
}
}
3、redisson工具类封装
@Component
@Slf4j
public class RedissonUtils {
@Autowired
private RedissonClient redissonClient;
/**
* 判断key是否存在
*
* @param key
* @return
*/
public boolean isExists(String key) {
var bucket = redissonClient.getBucket(key);
return bucket.isExists();
}
/**
* 对value模糊搜索
*
* @param key zset key
* @param value 搜索条件
* @return 前100条
*/
public List<String> getRelateCustomerWord(String key, String value) {
var scoredSortedSet = redissonClient.getScoredSortedSet(key);
if (scoredSortedSet.isEmpty()) {
return Lists.newArrayList();
}
// 无搜索条件,默认返回100条
if (StringUtil.isBlank(value)) {
var entries = scoredSortedSet.entryRange(0, 99);
return entries.stream().map(v -> v.getValue().toString()).collect(Collectors.toList());
}
// 模糊匹配value,获取100条
var iterator = scoredSortedSet.iterator("*" + value + "*", 100);
List<String> res = Lists.newArrayList();
while (iterator.hasNext()) {
var next = iterator.next();
res.add(String.valueOf(next));
}
return res;
}
/**
* 添加redisson分布式锁, 防止并发执行
*
* @param lockKey 锁名称
* @param waitTime 获取锁的等待时间 单位:秒
* @param leaseTime 持有锁时间 单位:秒
* @param process 执行器
* @return
*/
public Object lockExecute(String lockKey, long waitTime, long leaseTime, RedissonLockProcess process) throws Exception {
RLock lock = redissonClient.getLock(lockKey);
try {
// 加锁
boolean haveLock = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
// 获取锁
if (haveLock) {
try {
return process.execute();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
log.info("未获取到锁{},请重试", lockKey);
throw new Exception("未获取到锁" + lockKey);
}
} catch (InterruptedException e) {
log.info("尝试获取锁失败{}", lockKey, e);
throw new Exception("尝试获取锁失败" + lockKey);
}
}
}
// 函数式接口执行器
@FunctionalInterface
public interface RedissonLockProcess {
Object execute() throws MpException;
}
zset中使用iterator对scan命令进行封装,查看redisson源码可知
4、项目实际应用
@Autowired
private RestTemplateClient restTemplateClient;
// 获取可用颜色下拉数据
public List<String> getAcceptedColors(ShopAuthParam param) throws Exception {
var cacheColorKey = "color_zset";
var isExists = redissonUtils.isExists(cacheColorKey);
// 存在
if (isExists) {
// cache qry
return redissonUtils.getRelateCustomerWord(cacheColorKey, param.getKeyword());
} else {
// 此处使用分布式锁,保证数据只有一份,lockExecute方法在上边的工具类RedissonUtils已经封装好
return (List<String>) redissonUtils.lockExecute("lock:color", 3, 10, () -> {
// 再次判断是否已经添加进缓存
var dataExist = redissonUtils.isExists(cacheColorKey);
if (dataExist) {
// cache qry
return redissonUtils.getRelateCustomerWord(cacheColorKey, param.getKeyword());
} else {
// todo 从第三方接口获取
List<String> res = xxx;
// 存入redis zset
var scoredSortedSet = redissonClient.getScoredSortedSet(cacheColorKey);
var collect = res.stream().collect(Collectors.toMap(d -> (Object)d, v -> 0.0));
scoredSortedSet.addAll(collect);
// cache 12 hours
scoredSortedSet.expire(12, TimeUnit.HOURS);
return res.stream()
.filter(data -> data.contains(param.getKeyword()))
.limit(100).collect(Collectors.toList());
}
});
}
}
使用scoredSortedSet 而不使用SortedSet,LexSortedSet两种,是因为想增加分数累加功能,当使用某一下拉数据一次,就累加1,也就是在数据被使用后执行
scoredSortedSet.addScore(data, scoredSortedSet.getScore(data) + 1);
这样当用户使用某一下拉数据次数越多,他下次查询时显示的就越靠前;