百度人脸识别应用(三)

386 阅读3分钟

「这是我参与11月更文挑战的第 7 天,活动详情查看:2021最后一次更文挑战

1、前言

​ 在前两次的分享中,我介绍了人脸查找:百度人脸识别应用(一) - 掘金 (juejin.cn) 及人脸库的管理:百度人脸识别应用(二) - 掘金 (juejin.cn),但在实际开发中还面临着一个很严重的问题就是,之前的用户信息只是在服务器本地存储了,并没有添加到人脸库中管理,这样就会导致之前的用户可以绕过人脸查找,替考现象依旧会存在。并且,之前的存量用户数据大概在3-400w左右,同时同步数据不能影响线上环境的正常使用,所以决定把这个功能结合redis消息队列做成异步的批量同步。

2、批量同步数据至人脸库

​ 在实际开发过程中,我也使用到了生产者-消费者模式。整个功能大体思路是:生产者轮询每秒从数据库里读取1000条未同步的数据,将其放到redis消息队列里,然后结合多线程消费者依次消费数据,进行人脸信息同步操作。这里要特别注意的几个点是:1)由于是对线上数据进行操作,一定不能影响已有功能,同步失败的数据记录下来即可。2)受限于接口的QPS,同时不能影响用户的正常使用,所以要限制线程并发量。下面就结合代码具体说明下:

生产者:

/**

 * @Description: 准备需要同步到人脸库的人脸数据
   */
   public class SyncFaceDataProducer implements Runnable {

   @Override
   public void run() {
       produceSyncFaceData();
       return;
   }

   private void produceSyncFaceData() {
       while(true){
           try {
               //1、查询数据库中是否还有存量人脸数据(未注册到人脸库中的)
               Integer unregisteredFaceLibNum = syncFaceDataMapper.getUnregisteredFaceLibNum();
               Log4jUtil.logInfo.info("当前人脸信息中未注册到人脸库中的数量为:" + unregisteredFaceLibNum);
               if (unregisteredFaceLibNum > 0) {
                   if (RedisUtil.llenList(mqName) > 0) {
                       //暂停1s
                       Thread.sleep(1000);
                   } else {
                       appendFaceDataToMQ();
                   }
               }else {
                   return;
               }
               String flag = RedisUtil.getString(FaceGroupConatant.mq_cert_sync_face_flag);
               if (StringUtils.isNotBlank(flag)) {
                   return;
               }
           }catch (Exception e) {
               Log4jUtil.logError.error("实现Runnable接口的类,准备人靓存量数据错误:{}", e);
           }
       }
   }

   /**

    * 每次查5000条数据放redis消息队列里
    * @return
      */
      private void appendFaceDataToMQ() {
      //1、每次查5000条数据放redis消息队列里
      List<PhotoInfo> photoInfoNeedAddFaceLib = syncFaceDataMapper.getPhotoInfoNeedAddFaceLib();
      for (PhotoInfo photoInfo : photoInfoNeedAddFaceLib) {
          RedisUtil.rpushList(mqName, JSON.toJSONString(photoInfo));
      }
      }

}

消费者:

/**
 * @Description: 消费待注册到人脸库的数据
 */
    public class SyncFaceDataConsumer implements Runnable {
    
    @Override
    public void run() {
        while(true){
            try {
                if (threadPoolExecutor == null) {
                    threadPoolExecutor = new ThreadPoolExecutor(1, 1, 1, TimeUnit.DAYS, queue);
                }
                String threadCount = RedisUtil.getString("facer_sync_cert_num");
                if (threadCount == null) {
                    threadCount="1";
                }
                for(int i = 0; i < Integer.valueOf(threadCount); i++) {
                    if (threadPoolExecutor.getActiveCount() < Integer.valueOf(threadCount)) {
                        //queue.isEmpty()
                        String photoInfoStr = RedisUtil.blpopList(mqName);
                        PhotoInfo photoInfo = JSON.parseObject(photoInfoStr, PhotoInfo.class);
                        //设置日期格式
                        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                        if (photoInfoStr != null) {
                            Log4jUtil.logInfo.info(df.format(new Date()) + "-threadCount:" + threadCount + ",本次同步的用户信息:" + photoInfoStr);
                            MqCertFaceDataSyncRunnable poolRunnable = new MqCertFaceDataSyncRunnable(photoInfo, syncFaceDataMapper, faceGroupComponent);
                            threadPoolExecutor.execute(poolRunnable);
                        }
                    }
                }
            } catch (Exception e) {
                Log4jUtil.logError.error("实现Runnable接口的类,存量证件照注册人脸库队列执行错误:{}", e);
            }
        }
    }

    /**
     * 实现Runnable接口的类 身份证照注册到人脸库
     * @author
     */
     private class MqCertFaceDataSyncRunnable implements Runnable {

        @Override
        public void run() {
            try {
                MessageModel messageModel = faceGroupComponent.addUser(photoInfo.getId_card(), photoInfo.getPhoto_url());
                if (null != messageModel.getErrorCode() && messageModel.getErrorCode().equals(messageModel.constErrorCode)) {
                    recordSyncFaceDataFailInfo(photoInfo.getId_card(), messageModel.getErrMsg());
                }else {
                    //注册成功
                    Log4jUtil.logError.info("存量证件照注册人脸库成功,用户信息:{}" + photoInfo);
                    //将成功注册数量+1
                    RedisUtil.keyINCR(FaceGroupConatant.success_sync_face_data_num);
                }
            } catch (Exception e) {
                Log4jUtil.logError.error("实现Runnable接口的类,存量证件照注册人脸库错误:{},用户信息:" + photoInfo, e);
                recordSyncFaceDataFailInfo(photoInfo.getId_card(), e.getMessage());
            }
        }

        private void recordSyncFaceDataFailInfo(String certId, String errorMsg) {
            Log4jUtil.logError.info("存量证件照注册人脸库失败,用户信息:{}" + photoInfo + "错误信息:" + errorMsg);
            //记录注册失败日志信息
            syncFaceDataMapper.insertSyncFaceDataFailInfo(certId, errorMsg);
            //将注册失败数量+1
            RedisUtil.keyINCR(FaceGroupConatant.fail_sync_face_data_num);
        }
     }

}

3、功能入口:

/**
 * @Description: 将之前的存量人脸照片数据存入百度人脸库中
 */
	@Component
	public class SyncFaceDataComponent {

	/**
	 * 暂停同步存量人脸信息至人脸库中的操作
	 */
	 public void stopSyncFaceData() {
		//1、暂停准备数据的操作
		if (mqThreadProducer != null && mqThreadProducer.isAlive()) {
			RedisUtil.setString(FaceGroupConatant.mq_cert_sync_face_flag, "false");
		}
	 }

	/**
	 * 同步存量人脸信息至人脸库中
	 */
     public void syncFaceData() {
    	//0、将mq_cert_sync_face_flag标志位置空
		RedisUtil.delKey(FaceGroupConatant.mq_cert_sync_face_flag);
    	//1、准备数据
		execfaceDataPre();
		//2、消费者来消费数据
		execMQSyncFaceData();
     }

	/**
	 * 准备需要进行人脸注册的数据
	 * @throws InterruptedException
	 */
	 private synchronized void execfaceDataPre() {
		if (mqThreadProducer == null || !mqThreadProducer.isAlive()) {
			if (mQueueSyncFaceDataProducer == null) {
				mQueueSyncFaceDataProducer = new SyncFaceDataProducer(syncFaceDataMapper, FaceGroupConatant.mq_cert_sync_face_data_list);
			}
			mqThreadProducer = new Thread(mQueueSyncFaceDataProducer);
			mqThreadProducer.start();
		}
	 }

	/**
	 * 执行消息队列处理线程 ,如果已经启动则忽略
	 * */
	private synchronized void execMQSyncFaceData() {
		if (mqThreadConsumer == null) {
			if (mQueueSyncFaceDataConsumer == null) {
				mQueueSyncFaceDataConsumer = new SyncFaceDataConsumer(syncFaceDataMapper, FaceGroupConatant.mq_cert_sync_face_data_list, faceGroupComponent);
			}
			mqThreadConsumer = new Thread(mQueueSyncFaceDataConsumer);
			mqThreadConsumer.start();
		}
	}

	/**
	 * 查看人脸同步信息
	 * @return
	 */
     public String getSyncFaceDataInfo() {
		//1、目前还有多少未同步的存量数据
		Integer unregisteredFaceLibNum = syncFaceDataMapper.getUnregisteredFaceLibNum();
		//2、获取成功同步到人脸库的数量
		String successSyncFaceDataNum = RedisUtil.getString(FaceGroupConatant.success_sync_face_data_num);
		//3、获取同步失败到人脸库的数量
		String failSyncFaceDataNum = RedisUtil.getString(FaceGroupConatant.fail_sync_face_data_num);
		String resMsg = String.format("目前未同步用户量为:%d,已成功同步数据量为:%s,同步失败数据量为:%s", unregisteredFaceLibNum, successSyncFaceDataNum, failSyncFaceDataNum);
		return resMsg;
	 }

}

​ 最后,大家在使用到多线程时一定要注意对临界资源的处理,特别是这种批量更改线上数据的,一定要保证不能出现死锁等现象,对于处理失败的数据,记录在表中,后续统一处理即可。