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