Detección de puntos de acceso: qué puede hacer HotKey en IoT

I. Introducción

        Hoy, el blogger hablará sobre la detección de puntos de acceso en el campo de Internet de las cosas, y hablará sobre la arquitectura y el código fuente.El middleware en el que se basa es principalmente HotKey de JD Wu Weifeng (aquí ha sido aprobado por el creador) hotkey: JD App background middleware , detección a nivel de milisegundos Los datos activos se envían a la memoria del clúster de servidores en milisegundos, lo que reduce en gran medida la presión de las teclas de acceso rápido en la consulta de la capa de datos         .

        Suena un poco oficial. Echemos un vistazo a los escenarios en los que este middleware se puede usar en el Internet de las cosas y los campos de activos, y luego presentemos su principio de implementación, que es conveniente para la construcción y el desarrollo secundario.

2. IoT y escenarios de activos

1. Saltar

        La máquina provocará saltos en algunos métodos y combinaciones de uso específicos. De hecho, hay muchos contenidos generalizados, principalmente debido a acciones repetidas y continuas, como aflojar y apretar la bayoneta, y abrir y cerrar el interruptor de viaje repetidamente.

        Esto conducirá a un procesamiento frecuente e inválido de escenarios comerciales, como alerta temprana y reparación, lo que resultará en el consumo de una gran cantidad de latidos de IoT, desperdiciando rendimiento y recursos.

        De hecho, HotKey se puede usar aquí. Por ejemplo, si la regla está configurada para recibir más de cinco transiciones en dos segundos, se marcará como un problema, se detendrá el procesamiento inútil y, después de la identificación, se entregará al hardware para identificación y detección.

        Algunos estudiantes pueden decir, ¿puedo hacerlo yo mismo en el servicio?

        Sí, pero esto está sesgado hacia los requisitos técnicos y los problemas causados ​​no son serios. No es realista gastar tiempo y esfuerzo para hacer un requisito algo personalizado en lugar de general. Pero después de usar el marco, además del costo de construcción, operación y mantenimiento, todo lo demás es común y, como desarrollador de servicios, no le importan estas operaciones y mantenimiento, y es fácil de usar.

2. Tiempo de espera de consulta

        El rendimiento es muy importante para invocar los servicios de otras personas o llamar a sus propios servicios para otros. Para sistemas con requisitos de bajo rendimiento, como listas de tiendas, listas de máquinas y áreas de organización relacionadas, definitivamente solo se necesitan consultas de segundo nivel. De lo contrario, el servicio El equipo de gestión de calidad vendrá a Ud. Una gran parte de las consultas son irresolubles porque:

        1. Llamadas excesivas a sistemas externos

        2. Cuando se llama al sistema externo o se proporciona la consulta externa, el tráfico alcanza su punto máximo repentinamente y luego la E/S del disco o la red aumenta instantáneamente.

        En este caso, la tecla de acceso rápido también se puede usar para almacenar temporalmente en caché datos calientes y consultarlos directamente desde la memoria local, y luego SLA no causará problemas.

3. Principio

1. Detección de teclas de acceso rápido

1.1 Estrategia de detección

        Muchas personas en la comunidad de código abierto pensaron que su estrategia se basaba en la interceptación cuando no entendían este middleware al principio, como interceptar la solicitud enviada a redis y enviarla al servidor para calcular si es una tecla de acceso rápido.

        De hecho, no es así, y está dirigido a todas las teclas de acceso rápido, es decir, no distingue qué es el almacenamiento de terceros, si es es, db, redis, etc. almacenamiento de terceros, Puede calcular teclas de acceso rápido.

        Entonces, ¿cómo lo hizo? De hecho, lo consideró basado en el pensamiento empresarial. Su creador, Wu Weifeng, también es un desarrollador de negocios, no un equipo de middleware.

        Entonces, ¿cuál es el pensamiento empresarial? Si necesita exponer o contar un valor-clave al mundo exterior, entonces se le comprobará primero para ver si es una tecla de acceso rápido. Si lo es, obtendrá los datos del local caché En este momento, significa que se ha accedido a la clave una vez, y el conteo puede comenzar.

        Si hace suficiente calor, el método isHotKey se llamará muchas veces.

        

1.2 Clave de acceso del cliente

        El cliente es principalmente la clase JdHotKeyStore como punto de exposición en el paquete de dependencia. El más importante es el método isHotKey, que primero se cuenta y se guarda localmente, y no se enviará al servidor de inmediato.

public static boolean isHotKey(String key) {
    try {
        if (!inRule(key)) {
            return false;
        }
        boolean isHot = isHot(key);
        if (!isHot) {
            HotKeyPusher.push(key, null);
        } else {
            ValueModel valueModel = getValueSimple(key);
            //判断是否过期时间小于1秒,小于1秒的话也发送
            if (isNearExpire(valueModel)) {
                HotKeyPusher.push(key, null);
            }
        }

        //统计计数
        KeyHandlerFactory.getCounter().collect(new KeyHotModel(key, isHot));
        return isHot;
    } catch (Exception e) {
        return false;
    }

}
Básicamente ahorra 0,5 segundos y envíalo al servidor
public static void push(String key, KeyType keyType, int count, boolean remove) {
if (count <= 0) {
count = 1;
}
if (keyType == null) {
keyType = KeyType.REDIS_KEY;
}
if (key == null) {
return;
}

LongAdder adderCnt = new LongAdder();
adderCnt.add(count);

HotKeyModel hotKeyModel = new HotKeyModel();
hotKeyModel.setAppName(Context.APP_NAME);
hotKeyModel.setKeyType(keyType);
hotKeyModel.setCount(adderCnt);
hotKeyModel.setRemove(remove);
hotKeyModel.setKey(key);


if (remove) {
//如果是删除key,就直接发到etcd去,不用做聚合。但是有点问题现在,这个删除只能删手工添加的key,不能删worker探测出来的
//因为各个client都在监听手工添加的那个path,没监听自动探测的path。所以如果手工的那个path下,没有该key,那么是删除不了的。
//删不了,就达不到集群监听删除事件的效果,怎么办呢?可以通过新增的方式,新增一个热key,然后删除它
EtcdConfigFactory.configCenter().putAndGrant(HotKeyPathTool.keyPath(hotKeyModel), Constant.DEFAULT_DELETE_VALUE, 1);
EtcdConfigFactory.configCenter().delete(HotKeyPathTool.keyPath(hotKeyModel));
//也删worker探测的目录
EtcdConfigFactory.configCenter().delete(HotKeyPathTool.keyRecordPath(hotKeyModel));
} else {
//如果key是规则内的要被探测的key,就积累等待传送
if (KeyRuleHolder.isKeyInRule(key)) {
//积攒起来,等待每半秒发送一次
KeyHandlerFactory.getCollector().collect(hotKeyModel);
}
}
}


        Aquí, se almacena localmente en ConcurrentHashMap. La idea más ingeniosa es usar dos mapas para evitar que se escriban datos cuando el mapa obtiene datos y cambiar la clase atómica cada vez que se obtienen los datos.

public void collect(HotKeyModel hotKeyModel) {
    String key = hotKeyModel.getKey();
    if (StrUtil.isEmpty(key)) {
        return;
    }
    if (atomicLong.get() % 2 == 0) {
        //不存在时返回null并将key-value放入,已有相同key时,返回该key对应的value,并且不覆盖
        HotKeyModel model = map0.putIfAbsent(key, hotKeyModel);
        if (model != null) {
            model.add(hotKeyModel.getCount());
        }
    } else {
        HotKeyModel model = map1.putIfAbsent(key, hotKeyModel);
        if (model != null) {
            model.add(hotKeyModel.getCount());
        }
    }

}
public List<HotKeyModel> lockAndGetResult() {
    //自增后,对应的map就会停止被写入,等待被读取
    atomicLong.addAndGet(1);

    List<HotKeyModel> list;
    if (atomicLong.get() % 2 == 0) {
        list = get(map1);
        map1.clear();
    } else {
        list = get(map0);
        map0.clear();
    }
    return list;
}
Cuando se inicia el servicio, inicia una tarea programada y, después de extraer los datos del mapa, se enviará a través de netty.
public static void startPusher(Long period) {
    if (period == null || period <= 0) {
        period = 500L;
    }
    @SuppressWarnings("PMD.ThreadPoolCreationRule")
    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("hotkey-pusher-service-executor", true));
    scheduledExecutorService.scheduleAtFixedRate(() -> {
        IKeyCollector<HotKeyModel, HotKeyModel> collectHK = KeyHandlerFactory.getCollector();
        List<HotKeyModel> hotKeyModels = collectHK.lockAndGetResult();
        if(CollectionUtil.isNotEmpty(hotKeyModels)){
            KeyHandlerFactory.getPusher().send(Context.APP_NAME, hotKeyModels);
            collectHK.finishOnce();
        }

    },0, period, TimeUnit.MILLISECONDS);
}
public void send(String appName, List<HotKeyModel> list) {
    //积攒了半秒的key集合,按照hash分发到不同的worker
    long now = System.currentTimeMillis();

    Map<Channel, List<HotKeyModel>> map = new HashMap<>();
    for(HotKeyModel model : list) {
        model.setCreateTime(now);
        Channel channel = WorkerInfoHolder.chooseChannel(model.getKey());
        if (channel == null) {
            continue;
        }

        List<HotKeyModel> newList = map.computeIfAbsent(channel, k -> new ArrayList<>());
        newList.add(model);
    }

    for (Channel channel : map.keySet()) {
        try {
            List<HotKeyModel> batch = map.get(channel);
            HotKeyMsg hotKeyMsg = new HotKeyMsg(MessageType.REQUEST_NEW_KEY, Context.APP_NAME);
            hotKeyMsg.setHotKeyModels(batch);
            channel.writeAndFlush(hotKeyMsg).sync();
        } catch (Exception e) {
            try {
                InetSocketAddress insocket = (InetSocketAddress) channel.remoteAddress();
                JdLogger.error(getClass(),"flush error " + insocket.getAddress().getHostAddress());
            } catch (Exception ex) {
                JdLogger.error(getClass(),"flush error");
            }

        }
    }

}

1.3 Cómputo del servidor

        El servidor escucha el evento netty, y si es un acceso de nueva clave, ingresa el método newKey de KeyListener

public void newKey(HotKeyModel hotKeyModel, KeyEventOriginal original) {
//cache里的key
String key = buildKey(hotKeyModel);
//判断是不是刚热不久
Object o = hotCache.getIfPresent(key);
if (o != null) {
return;
}

SlidingWindow slidingWindow = checkWindow(hotKeyModel, key);
//看看hot没
boolean hot = slidingWindow.addCount(hotKeyModel.getCount());

if (!hot) {
//如果没hot,重新put,cache会自动刷新过期时间
CaffeineCacheHolder.getCache(hotKeyModel.getAppName()).put(key, slidingWindow);
} else {
hotCache.put(key, 1);

//删掉该key
CaffeineCacheHolder.getCache(hotKeyModel.getAppName()).invalidate(key);

//开启推送
hotKeyModel.setCreateTime(SystemClock.now());

//当开关打开时,打印日志。大促时关闭日志,就不打印了
if (EtcdStarter.LOGGER_ON) {
logger.info(NEW_KEY_EVENT + hotKeyModel.getKey());
}

//分别推送到各client和etcd
for (IPusher pusher : iPushers) {
pusher.push(hotKeyModel);
}

}

}



        Lo principal es abrir una ventana deslizante SlidingWindow. La clave son las reglas de configuración correspondientes a los dos prefijos clave. Uno es el intervalo y el otro es la cantidad, como la clave de falla de la máquina, 2s5 visitas y luego el método addCount. de SlidingWindow determina si se ha vuelto caliente o no

        .

public synchronized boolean addCount(long count) {
        //当前自己所在的位置,是哪个小时间窗
        int index = locationIndex();
//        System.out.println("index:" + index);
        //然后清空自己前面windowSize到2*windowSize之间的数据格的数据
        //譬如1秒分4个窗口,那么数组共计8个窗口
        //当前index为5时,就清空6、7、8、1。然后把2、3、4、5的加起来就是该窗口内的总和
        clearFromIndex(index);

        int sum = 0;
        // 在当前时间片里继续+1
        sum += timeSlices[index].addAndGet(count);
        //加上前面几个时间片
        for (int i = 1; i < windowSize; i++) {
            sum += timeSlices[(index - i + timeSliceSize) % timeSliceSize].get();
        }

        lastAddTimestamp = SystemClock.now();

        return sum >= threshold;
    }

1.4 Cliente de envío de teclas de acceso rápido

        Una vez que se calcula la tecla de acceso rápido, no se envía al cliente de inmediato. Primero se colocará en la cola y luego se presionará cada 10 ms. De hecho, para una empresa con un tamaño pequeño, esto es realmente innecesario.

public void batchPushToClient() {
    AsyncPool.asyncDo(() -> {
        while (true) {
            try {
                List<HotKeyModel> tempModels = new ArrayList<>();
                //每10ms推送一次
                Queues.drain(hotKeyStoreQueue, tempModels, 10, 10, TimeUnit.MILLISECONDS);
                if (CollectionUtil.isEmpty(tempModels)) {
                    continue;
                }

                Map<String, List<HotKeyModel>> allAppHotKeyModels = new HashMap<>();

                //拆分出每个app的热key集合,按app分堆
                for (HotKeyModel hotKeyModel : tempModels) {
                    List<HotKeyModel> oneAppModels = allAppHotKeyModels.computeIfAbsent(hotKeyModel.getAppName(), (key) -> new ArrayList<>());
                    oneAppModels.add(hotKeyModel);
                }

                //遍历所有app,进行推送
                for (AppInfo appInfo : ClientInfoHolder.apps) {
                    List<HotKeyModel> list = allAppHotKeyModels.get(appInfo.getAppName());
                    if (CollectionUtil.isEmpty(list)) {
                        continue;
                    }

                    HotKeyMsg hotKeyMsg = new HotKeyMsg(MessageType.RESPONSE_NEW_KEY);
                    hotKeyMsg.setHotKeyModels(list);

                    //整个app全部发送
                    appInfo.groupPush(hotKeyMsg);
                }

                allAppHotKeyModels = null;

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

1.5 El cliente almacena en caché las teclas de acceso rápido

        Luego regresa al cliente. El cliente recibe el evento de tecla de acceso rápido y lo escribe en el caché local. Aquí se usa la cafeína, el rey del rendimiento del caché. Si es una escena fija, se puede reescribir. Al agregar el caché , verifique el valor de acuerdo con la clave. Luego configúrelo, de modo que no haya necesidad de configurar el valor cuando el cliente juzgue que es una tecla de acceso rápido y no tiene valor.

public void newKey(HotKeyModel hotKeyModel) {
    long now = System.currentTimeMillis();
    //如果key到达时已经过去1秒了,记录一下。手工删除key时,没有CreateTime
    if (hotKeyModel.getCreateTime() != 0 && Math.abs(now - hotKeyModel.getCreateTime()) > 1000) {
        JdLogger.warn(getClass(), "the key comes too late : " + hotKeyModel.getKey() + " now " +
                +now + " keyCreateAt " + hotKeyModel.getCreateTime());
    }
    if (hotKeyModel.isRemove()) {
        //如果是删除事件,就直接删除
        deleteKey(hotKeyModel.getKey());
        return;
    }
    //已经是热key了,又推过来同样的热key,做个日志记录,并刷新一下
    if (JdHotKeyStore.isHot(hotKeyModel.getKey())) {
        JdLogger.warn(getClass(), "receive repeat hot key :" + hotKeyModel.getKey() + " at " + now);
    }
    addKey(hotKeyModel.getKey());
}
private void addKey(String key) {
    ValueModel valueModel = ValueModel.defaultValue(key);
    if (valueModel == null) {
        //不符合任何规则
        deleteKey(key);
        return;
    }
    //如果原来该key已经存在了,那么value就被重置,过期时间也会被重置。如果原来不存在,就新增的热key
    JdHotKeyStore.setValueDirectly(key, valueModel);
}

2. Almacenamiento etc.

        Como almacenamiento persistente, Etcd utiliza principalmente teclas de acceso rápido para evitar la pérdida de teclas de acceso rápido causada por una falla o liberación del punto único del servicio, y también necesita almacenar reglas de teclas de acceso rápido. Cada nodo del servidor también pondrá su propia información de IP en etcd, para que el cliente pueda obtener la información del servidor para netty push.

2.1 El cliente actualiza la tecla de acceso rápido

        Actualice el caché local monitoreando los cambios de datos de etcd en la ruta correspondiente

private void startWatchHotKey() {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.submit(() -> {
        JdLogger.info(getClass(), "--- begin watch hotKey change ----");
        IConfigCenter configCenter = EtcdConfigFactory.configCenter();
        try {
            KvClient.WatchIterator watchIterator = configCenter.watchPrefix(ConfigConstant.hotKeyPath + Context.APP_NAME);
            //如果有新事件,即新key产生或删除
            while (watchIterator.hasNext()) {
                WatchUpdate watchUpdate = watchIterator.next();

                List<Event> eventList = watchUpdate.getEvents();
                KeyValue keyValue = eventList.get(0).getKv();
                Event.EventType eventType = eventList.get(0).getType();
                try {
                    String key = keyValue.getKey().toStringUtf8().replace(ConfigConstant.hotKeyPath + Context.APP_NAME + "/", "");

                    //如果是删除key,就立刻删除
                    if (Event.EventType.DELETE == eventType) {
                        HotKeyModel model = new HotKeyModel();
                        model.setRemove(true);
                        model.setKey(key);
                        EventBusCenter.getInstance().post(new ReceiveNewKeyEvent(model));
                    } else {
                        HotKeyModel model = new HotKeyModel();
                        model.setRemove(false);
                        String value = keyValue.getValue().toStringUtf8();
                        //新增热key
                        JdLogger.info(getClass(), "etcd receive new key : " + key + " --value:" + value);
                        //如果这是一个删除指令,就什么也不干
                        if (Constant.DEFAULT_DELETE_VALUE.equals(value)) {
                            continue;
                        }

                        //手工创建的value是时间戳
                        model.setCreateTime(Long.valueOf(keyValue.getValue().toStringUtf8()));

                        model.setKey(key);
                        EventBusCenter.getInstance().post(new ReceiveNewKeyEvent(model));
                    }
                } catch (Exception e) {
                    JdLogger.error(getClass(), "new key err :" + keyValue);
                }

            }
        } catch (Exception e) {
            JdLogger.error(getClass(), "watch err");
        }
    });

}

2.2 Reglas de actualización de clientes

private void startWatchRule() {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.submit(() -> {
        JdLogger.info(getClass(), "--- begin watch rule change ----");
        try {
            IConfigCenter configCenter = EtcdConfigFactory.configCenter();
            KvClient.WatchIterator watchIterator = configCenter.watch(ConfigConstant.rulePath + Context.APP_NAME);
            //如果有新事件,即rule的变更,就重新拉取所有的信息
            while (watchIterator.hasNext()) {
                //这句必须写,next会让他卡住,除非真的有新rule变更
                WatchUpdate watchUpdate = watchIterator.next();
                List<Event> eventList = watchUpdate.getEvents();
                JdLogger.info(getClass(), "rules info changed. begin to fetch new infos. rule change is " + eventList);

                //全量拉取rule信息
                fetchRuleFromEtcd();
            }
        } catch (Exception e) {
            JdLogger.error(getClass(), "watch err");
        }


    });
}
 
 

2.3 El servidor comprueba que está en etcd

public void makeSureSelfOn() {
    //开启上传worker信息
    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
    scheduledExecutorService.scheduleAtFixedRate(() -> {

        try {
            if (canUpload) {
                uploadSelfInfo();
            }
        } catch (Exception e) {
            //do nothing
        }

    }, 0, 5, TimeUnit.SECONDS);
}
private void uploadSelfInfo() {
    configCenter.putAndGrant(buildKey(), buildValue(), 8);
}

2.4 Verifique la dirección que se muestra en la parte frontal, porque la parte frontal de la tecla de acceso rápido está hecha con jsp

@Scheduled(fixedRate = 30000)
public void fetchDashboardIp() {
    try {
        //获取DashboardIp
        List<KeyValue> keyValues = configCenter.getPrefix(ConfigConstant.dashboardPath);

        //是空,给个警告
        if (CollectionUtil.isEmpty(keyValues)) {
            logger.warn("very important warn !!! Dashboard ip is null!!!");
            return;
        }

        String dashboardIp = keyValues.get(0).getValue().toStringUtf8();
        NettyClient.getInstance().connect(dashboardIp);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

3. Pantalla frontal y configuración de reglas

3.1 El servidor envía el tablero

        Cuando se presiona la tecla de acceso rápido, además de presionar al cliente, también se presionará al tablero

@PostConstruct
public void uploadToDashboard() {
    AsyncPool.asyncDo(() -> {
        while (true) {
            try {
                //要么key达到1千个,要么达到1秒,就汇总上报给etcd一次
                List<HotKeyModel> tempModels = new ArrayList<>();
                Queues.drain(hotKeyStoreQueue, tempModels, 1000, 1, TimeUnit.SECONDS);
                if (CollectionUtil.isEmpty(tempModels)) {
                    continue;
                }

                //将热key推到dashboard
                DashboardHolder.flushToDashboard(FastJsonUtils.convertObjectToJSON(tempModels));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

3.2 El tablero recibe la tecla de acceso rápido

        Después de recibir el evento de lectura a través de netty, guárdelo en la cola

protected void channelRead0(ChannelHandlerContext ctx, String message) {
    if (StringUtils.isEmpty(message)) {
        return;
    }
    try {
        HotKeyMsg msg = FastJsonUtils.toBean(message, HotKeyMsg.class);
        if (MessageType.PING == msg.getMessageType()) {
            String hotMsg = FastJsonUtils.convertObjectToJSON(new HotKeyMsg(MessageType.PONG, PONG));
            FlushUtil.flush(ctx, MsgBuilder.buildByteBuf(hotMsg));
        } else if (MessageType.REQUEST_HOT_KEY == msg.getMessageType()) {
            List<HotKeyModel> list = FastJsonUtils.toList(msg.getBody(), HotKeyModel.class);
            for (HotKeyModel hotKeyModel : list) {
                HotKeyReceiver.push(hotKeyModel);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

}

        Luego, los datos en la cola se consumirán continuamente y una copia de los datos se colocará en la cola de bloqueo y en la memoria caché local, respectivamente.

public void dealHotKey() {
    while (true) {
        try {
            HotKeyModel model = HotKeyReceiver.take();
            //将该key放入实时热key本地缓存中
            if (model != null) {
                //将key放到队列里,供入库时分批调用
                putRecord(model.getAppName(), model.getKey(), model.getCreateTime());
                //获取发来的这个热key,存入本地caffeine,设置过期时间
                HotKeyReceiver.writeToLocalCaffeine(model);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

        Otro subproceso asíncrono continuará insertando datos en mysql

public void insertRecords() {
        while (true) {
            try {
                List<IRecord> records = new ArrayList<>();
                Queues.drain(queue, records, 1000, 1, TimeUnit.SECONDS);
                if (CollectionUtil.isEmpty(records)) {
                    continue;
                }
                List<KeyRecord> keyRecordList = new ArrayList<>(records.size());
                for (IRecord iRecord : records) {
                    KeyRecord keyRecord = handHotKey(iRecord);
                    if (keyRecord != null) {
                        keyRecordList.add(keyRecord);
                    }
                }
                if(CollectionUtil.isEmpty(keyRecordList)){
                    continue;
                }
                keyRecordMapper.batchInsert(keyRecordList);

            } catch (Exception e) {
                log.error("batch insert error:{}", e.getMessage(), e);
//                e.printStackTrace();
            }
        }

    }

3.3 Reglas de teclas de acceso rápido

        La entrada está en RuleController

@PostMapping("/save")
@ResponseBody
public Result save(Rules rules){
   checkApp(rules.getApp());
   checkRule(rules.getRules());
   rules.setUpdateUser(userName());
   int b = ruleService.save(rules);
   return b == 0 ? Result.fail():Result.success();
}

        Al guardar las reglas, primero se insertará una copia en etcd, de modo que tanto el cliente como el servidor puedan obtener las reglas más recientes al monitorear etcd

public int save(Rules rules) {
    String app = rules.getApp();

    KeyValue kv = configCenter.getKv(ConfigConstant.rulePath + app);
    String from = null;
    if (kv != null) {
        from = kv.getValue().toStringUtf8();
    }
    String to = JSON.toJSONString(rules);
    configCenter.put(ConfigConstant.rulePath + app, rules.getRules());

    logMapper.insertSelective(new ChangeLog(app, 1, from, to, rules.getUpdateUser(), app, SystemClock.nowDate()));
    return 1;
}

Cuatro Resumen

        Hotkey ha realizado un gran desarrollo detallado para la detección de puntos de acceso. Si hay escenarios comerciales que deben cambiarse, se puede realizar un desarrollo secundario y los bloggers pueden ayudar a discutirlo. Aquí, me gustaría agradecer a JD Wu Weifeng, el creador de tecla de acceso rápido, de nuevo.

Supongo que te gusta

Origin blog.csdn.net/m0_69270256/article/details/130826388
Recomendado
Clasificación