一、实现方式简要说明
1.1、词源存储
便于测试,本地使用中文+英文随机生成100k的词量,根据要求词的平均长度为15。
使用txt文件存储到本地,文件大小:2.64M,也可以使用其他文件格式,如csv文件。
1.2、预加载
由于读取词文本并构建词树需要消耗一定的时间(本地读取txt文件并构建词树耗时:10~15秒之间),我们在项目启动进行关键词树预加载,并通过本地+redis进行缓存,提高词树的读取速度。
1.3、缓存时间设置
为了测试,缓存有效时间设置为60秒(本地随机生成词在redis存储大小:22.07M),实际应用建议持久化存储,添加缓存刷新机制。
1.4、关键词检验流程
1、接口接收检验文本
2、本地缓存获取词树,不存在则从redis获取,获取成功之后缓存到本地
3、redis不存在,则读取词文本文件,构建词树,缓存到本地及redis
4、调用acdat的parseText方法进行关键词匹配
1.5、刷新缓存
一般情况下,词文本变动的情况比较少,但是不排除更新的情况,为了缓存能和词文本内容及时保持一致,提供如下两种方式将缓存进行同步更新:
-
添加监听器监听词文本状态,当词文本内容发生变化则读取词文本内容,更新本地及redis缓存(推荐方式)
-
添加定时任务(此方案允许存在一定的延时性)检测词文本状态,如果进行了更新,则读取词文本内容,更新本地及redis缓存
1.6、Jmeter测试
测试参数 | 测试值 | |
---|---|---|
检验文本 | 2K字 | |
并发数 | 200 | |
持续时间 | 120秒 |
二、代码实现
1、项目引入依赖
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>aho-corasick-double-array-trie</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.4</version>
</dependency>
2、配置文件添加 redis配置(略)
redis:
host: redis服务IP
port: redis端口
password: 密码
timeout: 6000ms
database: 2
3、添加缓存时间配置
keywords:
time-out: 60
words-key: "keyWords"
chinese-chars: “生成词的中文”
english-chars: “生成词的英文”
4、添加本地缓存配置
@Configuration
public class CaffeineConfig {
@Value("${keywords.time-out}")
private Integer keyWorksTimeOut;
@Bean
@Primary
public CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
ArrayList<CaffeineCache> caches = Lists.newArrayList();
Map<String, Object> map = getCacheType();
for (String name : map.keySet()) {
caches.add(new CaffeineCache(name, (Cache<Object, Object>) map.get(name)));
}
cacheManager.setCaches(caches);
return cacheManager;
}
private Map<String, Object> getCacheType() {
Map<String, Object> map = new ConcurrentHashMap<>();
map.put("key-words", Caffeine.newBuilder().recordStats()
.expireAfterWrite(keyWorksTimeOut, TimeUnit.SECONDS)
.maximumSize(100)
.build());
return map;
}
}
5、添加redis配置
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
6、添加监听器,项目启动执行
@Component
public class KeyWordsListener {
@Autowired
public KeywordService keywordService;
@Value("${keywords.words-key}")
private String WORDS_KEY;
@EventListener(classes = {ContextRefreshedEvent.class})
public void onApplicationEvent() {
keywordService.readKeyWords(“randomKeWords.txt”, WORDS_KEY);
}
}
7、添加redisService
@Service
public class RedisService {
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value){
redisTemplate.opsForValue().set(key, value);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key){
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 是否存在 Key
* @param key
* @return
*/
public boolean hasKey(String key){
return redisTemplate.hasKey(key);
}
8、添加关键词处理service
@Service
public class KeywordService {
@Resource
private RedisService redisService;
// 关键词缓存过期时间,单位:秒
@Value("${keywords.time-out}")
private Integer keyWorksTimeOut;
@Value("${keywords.words-key}")
private String WORDS_KEY;
@Value("${keywords.chinese-chars}")
private String CHINESE_CHARS;
@Value("${keywords.english-chars}")
private String ENGLISH_CHARS;
/**
* 随机生成词,保存到文本
* @return boolean,成功或失败
*/
public boolean randomKeyWords() {
Random random = new Random();
List<String> keywords = new ArrayList<>();
int totalKeywords = 100000; // 100k关键词
for (int i = 0; i < totalKeywords; i++) {
// 每个词的平均长度为15,但这里我们直接指定长度为15
String keyword = generateRandomKeyword(15, random);
keywords.add(keyword);
}
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(“randomKeWords.txt”),
StandardCharsets.UTF_8
)
)) {
for (String s : keywords) {
writer.write(s);
writer.newLine();
}
return true;
} catch (IOException e) {
e.printStackTrace();
System.out.println("在保存文件时发生错误");
}
return false;
}
/**
* 随机生成词(中文和英文混合)
* @param length
* @param random
* @return StringBuilder,词拼成的字符串
*/
public String generateRandomKeyword(int length, Random random) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
if (random.nextBoolean()) {
if (i > 0 && random.nextFloat() < 0.2) {
// 20% 的概率插入全角空格(可选)
sb.append(""); // 全角空格
} else {
// 插入随机中文字符
int index = random.nextInt(CHINESE_CHARS.length());
sb.append(CHINESE_CHARS.charAt(index));
}
} else {
// 插入一个英文字符(包括字母和数字)
int index = random.nextInt(ENGLISH_CHARS.length());
sb.append(ENGLISH_CHARS.charAt(index));
}
}
return sb.toString();
}
/**
* 读取关键词
* @param filePath
* @return ArrayTrie<String>
*/
@Cacheable(value = "key-words", key = "#key", sync = true)
public AhoCorasickDoubleArrayTrie<String> readKeyWords(String filePath, String key) {
AhoCorasickDoubleArrayTrie<String> arrayTrie = null;
// 二级redis缓存
if(redisService.hasKey(WORDS_KEY)){
arrayTrie = redisService.getCacheObject(WORDS_KEY);
// redis 存在,进行本地一级缓存
putKeyWords(arrayTrie,false);
return arrayTrie;
}
// 一级、二级缓存都不存在,先读取文本词,再进行缓存
arrayTrie = getArrayTrie(filePath);
// 进行一级、二级缓存
putKeyWords(arrayTrie, true);
return arrayTrie;
}
/**
*
* @param arrayTrie 词树
* @param isPutRedis 是否更新redis
*/
@CachePut(value = "key-words", key = "#arrayTrie")
public void putKeyWords(AhoCorasickDoubleArrayTrie<String> arrayTrie,Boolean isPutRedis){
if(isPutRedis){
// redis缓存
//redisService.setCacheObject(WORDS_KEY, arrayTrie, keyWorksTimeOut, TimeUnit.SECONDS);
redisService.setCacheObject(WORDS_KEY, arrayTrie);
}
}
/**
* 从保存的文本获取词
* @param filePath
* @return ArrayTrie<String>
*/
private AhoCorasickDoubleArrayTrie<String> getArrayTrie(String filePath){
Map<String,String> keyWordsMap = new HashMap<>();
boolean hasWords = false;
try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath))) {
String line;
while ((line = reader.readLine()) != null) {
keyWordsMap.put(line.trim(),line.trim());
hasWords = true;
}
} catch (IOException e) {
e.printStackTrace();
}
if(hasWords) {
AhoCorasickDoubleArrayTrie<String> arrayTrie = new AhoCorasickDoubleArrayTrie<String>();
arrayTrie.build(keyWordsMap);
return arrayTrie;
}else {
return null;
}
}
}
9、添加controller
@RestController
@RequestMapping("/checkWords")
public class CheckWords {
@Autowired
private KeywordService keywordService;
@Value("${keywords.words-key}")
private String WORDS_KEY;
@GetMapping("/checkFormKeyWords")
public String checkFormKeyWords(@RequestParam("sourceStr") String sourceStr){
Date start = new Date();
// 获取关键词
AhoCorasickDoubleArrayTrie<String> acdat = keywordService.readKeyWords(“randomKeWords.txt”, WORDS_KEY);
List<AhoCorasickDoubleArrayTrie.Hit<String>> wordList = acdat.parseText(sourceStr);
String timeStr = "startTime:"+start.getTime()+",nedTime:"+new Date().getTime();
if(null != wordList && wordList.size() > 0){
StringBuilder result = new StringBuilder(timeStr+",检验不通过,包含关键词:");
for (AhoCorasickDoubleArrayTrie.Hit<String> word: wordList
) {
result.append(word).append("、");
}
return result.toString();
}else {
return timeStr+",校验通过,无关键词";
}
}
/**
* 随机生成关键词
*/
@PostMapping("/randomKeyWords")
public String randomKeyWords(){
// 保存随机生成的字符到 txt
if(keywordService.randomKeyWords()){
return "关键词生成成功";
}
return "关键词生成失败";
}
}
10、添加监听器监听词文本更新状态
自实现
11、启动类添加缓存配置
@SpringBootApplication
@EnableCaching // Caffeine本地缓存
public class ServiceAcApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceAcApplication.class, args);
}
}