Преобразование китайских иероглифов в пиньинь с помощью многопоточного запроса миллионов пользовательских данных

Java задняя часть база данных

Теперь есть требование: в пользовательской таблице почти 2 миллиона фрагментов данных, и запрос нужно отсортировать от а до я по китайскому пиньину имени пользователя. Есть два решения: 1. При запросе используйте функцию CONVERT(), поставляемую с базой данных, для преобразования и сортировки по первой букве пиньинь 2. Добавьте новое поле пиньинь (spell_name), когда пользователь зарегистрируется, китайский пиньинь имени пользователя также вставляется в базу данных вместе. Взвесив, я принял второй, потому что количество пользователей будет продолжать расти, использование функций, которые идут в комплекте с базой данных, будет замедлять скорость запросов, а индекс также будет давать сбои. объем данных относительно велик, используя многопоточность, запишите его здесь.

1. Используйте jpinyin и emoji-java для преобразования китайских иероглифов в пиньинь.

Импорт связанных банок

		<!--汉字转拼音jar-->
		<dependency>
			<groupId>com.github.stuxuhai</groupId>
			<artifactId>jpinyin</artifactId>
			<version>1.0</version>
		</dependency>
		<!--java操作emoji的jar-->
		<dependency>
			<groupId>com.vdurmont</groupId>
			<artifactId>emoji-java</artifactId>
			<version>4.0.0</version>
		</dependency>

Класс инструментов обработки выражений эмодзи

public class EmojiDealUtil extends EmojiParser {
    /**
     * 获取非表情字符串
     * @param input
     * @return
     */
    public static String getNonEmojiString(String input) {
        int prev = 0;
        StringBuilder sb = new StringBuilder();
        List<UnicodeCandidate> replacements = getUnicodeCandidates(input);
        for (UnicodeCandidate candidate : replacements) {
            sb.append(input.substring(prev, candidate.getEmojiStartIndex()));
            prev = candidate.getFitzpatrickEndIndex();
        }
        return sb.append(input.substring(prev)).toString();
    }

    /**
     * 获取表情字符串
     * @param input
     * @return
     */
    public static String getEmojiUnicodeString(String input){
        EmojiTransformer  transformer = new EmojiTransformer() {
            public String transform(UnicodeCandidate unicodeCandidate) {
                return unicodeCandidate.getEmoji().getHtmlHexadecimal();
            }
        };
        StringBuilder sb = new StringBuilder();
        List<UnicodeCandidate> replacements = getUnicodeCandidates(input);
        for (UnicodeCandidate candidate : replacements) {
            sb.append(transformer.transform(candidate));
        }
        return  parseToUnicode(sb.toString());
    }

    public static String getUnicode(String source){
        String returnUniCode=null;
        String uniCodeTemp=null;
        for(int i=0;i<source.length();i++){
            uniCodeTemp = "\\u"+Integer.toHexString((int)source.charAt(i));
            returnUniCode=returnUniCode==null?uniCodeTemp:returnUniCode+uniCodeTemp;
        }
        return returnUniCode;
    }
}

Инструменты для преобразования китайских иероглифов в пиньинь

public class ChineseToPinYinUtil {

    /**
     * 转换为不带音调的拼音字符串
     * @param pinYinStr 需转换的汉字
     * @return 拼音字符串
     */
    public static String changeToTonePinYin(String pinYinStr) {
        String tempStr = null;
        try {
            tempStr = PinyinHelper.convertToPinyinString(pinYinStr, " ", PinyinFormat.WITHOUT_TONE);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return tempStr;
    }
}

Преобразование пиньинь не является предметом этой статьи.В Интернете есть много библиотек и кодовых решений, в основном следующая многопоточная обработка.

2. Используйте многопоточность для запроса и обновления базы данных.

Создайте пул потоков с начальной емкостью 5. Каждый поток запрашивает и обрабатывает 500 записей, добавляет блокировку синхронизации и назначает каждому потоку начальную запись своего запроса. После запроса записи вызовите описанный выше метод для преобразования китайских символов в пиньинь. , После обработки обновите базу данных после завершения.

2.1 Метод получения запроса

    //每个线程每次查询的条数
    private static final Integer LIMIT = 500;
    //起的线程数
    private static final Integer THREAD_NUM = 5;
    ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_NUM, THREAD_NUM*2,0,TimeUnit.SECONDS,new LinkedBlockingQueue<>(100));
    
    @GetMapping("/chineseToSpellName")
    public void execute(){
        //计数器,一次转换只能一个请求调,不然会出错
        int count = 0;
        logger.info("trans start");
        //查询总记录数
        int total = userService.getTotalCount2();
        logger.info("total num:{}",total);
        int num = total/(LIMIT*THREAD_NUM) + 1;
        logger.info("要经过的轮数:{}",num);
        for(int j=0;j<num;j++){
            //起 THREAD_NUM 个线程并行查询更新库,加锁
            for(int i=0;i<THREAD_NUM;i++){
                synchronized(ChineseToPinYinController.class){
                    int start = count*LIMIT;
                    count++;
                    pool.submit(new TransTask(start,LIMIT));
                }
            }
        }
    }

2.2 Бизнес-методы многопоточности

    class TransTask implements Runnable{
        int start;
        int limit;
        public TransTask(int start, int limit) {
            this.start = start;
            this.limit = limit;
        }

        @Override
        public void run() {
            //查询记录并更新数据库
            List<User> userList =  userService.getList2(start,limit);
            logger.info("更新记录起始位置:{}--{}",start,limit);
            if(!CollectionUtils.isEmpty(userList)){
                userList.stream().forEach(u -> {
                    u.setSpellName(ChineseToPinYinUtil.changeToTonePinYin(EmojiDealUtil.getNonEmojiString(u.getName())).trim());
                    userService.updateUser2(u);
                }
             );
            }
        }
    }

3. Не используйте традиционное ограничение пейджинга для запроса данных

userService.getList2(start,num) предназначен для запроса записей в соответствии с начальной позицией и количеством запросов Запросы на подкачку, которые мы писали в прошлом, обычно записывались так: select * from table limit start, num (например: select * от пользовательского лимита 0, 20). С таким запросом проблем нет, когда объем данных небольшой, но когда объем данных большой, запрос будет очень медленным, потому что он использует не индекс, а полное сканирование таблицы. объем данных, тем ниже скорость. Для запроса, идентификатор которого является самовозрастающим, можно использовать другой метод запроса, выбрать * из таблицы, где идентификатор > начальный предельный номер (например: выбрать * из пользователя, где идентификатор> 1000 предел 20), и запросить число записей из указанного идентификатора . Даже если объем данных в таком запросе достигнет миллионов, скорость выполнения запроса не будет значительно медленнее, поскольку вместо полного сканирования таблицы используется индекс первичного ключа.

4. Оптимизационный постскриптум

После того, как код написан, при фактическом использовании, когда данные инициализированы до более чем 700 000 штук, количество подключений к базе данных слишком велико, что заполняет всю базу данных Рассмотрите возможность повторной оптимизации, примите метод сегментации и пропустите в двух параметрах, записи инициализации и номере инициализации. Например, от 0 до 100 000 записей инициализируются в первый раз, от 100 000 до 200 000 записей — во второй раз и т. д.:

   //每个线程每次查询的条数
    private static final Integer LIMIT = 500;
    //起的线程数
    private static final Integer THREAD_NUM = 5;
    ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_NUM,Integer.MAX_VALUE,0,TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(10));

    @GetMapping("/chineseToSpellName")
    public void execute(@RequestParam("startId") Integer startId,@RequestParam("total") Integer total){
        logger.info("trans start");
        int num = total/(LIMIT*THREAD_NUM) + 1;
        logger.info("要经过的轮数:{}",num);
        for(int j=0;j<num;j++){
            //起 THREAD_NUM 个线程并行查询更新库,加锁
            for(int i=0;i<THREAD_NUM;i++){
                synchronized(ChineseToPinYinController.class){
                    pool.submit(new TransTask(startId,LIMIT));
                    startId+=LIMIT;
                }
            }
        }
    }

    class TransTask implements Runnable{
        int start;
        int limit;
        public TransTask(int start, int limit) {
            this.start = start;
            this.limit = limit;
        }

        @Override
        public void run() {
            //查询记录并更新数据库
            List<User> userList =  userService.getList2(start,limit);
            logger.info("更新记录起始位置:{}--{}",start,limit);
            if(!CollectionUtils.isEmpty(userList)){
                userList.stream().forEach(u -> {
                    u.setSpellName(ChineseToPinYinUtil.changeToTonePinYin(EmojiDealUtil.getNonEmojiString(u.getName())).trim());
                    userService.updateUser2(u);
                }
             );
            }
        }
    }