设备多久(60/50/40min)未上报,类似场景发送通知实现方案

news2024/10/10 10:58:35

场景描述

设备比较多,几十万甚至上百万,设备在时不时会上报消息。
用户可以设置设备60分钟、50分钟、40分钟、30分钟未上报数据,发送通知给用户,消息要及时可靠。

基本思路

思路:

由于设备在一直上报,如果直接存储到数据库对数据库的压力比较大,考虑使用缓存,每次上报都更新到缓存中; 由于是多久未上报发通知,考虑使用定时任务查找超过60/50/40/30min的设备;定时任务遍历时要尽可能少的查询设备缓存,因为绝大多数设备是不需要进行通知的,最好是只遍历需要发送通知的设备缓存,可以考虑使用类似于时间窗口机制,将设备缓存按时间进行分割,建立两个缓存,缓存1设备数据指向缓存2(主要用于实现设备数据在缓存2不用时间窗口转换),缓存2数据,用于定时任务数据扫描;考虑到消息通知的及时性,考虑使用延迟定时任务,来及时发送消息通知。由于设备比较大,考虑对缓存1按hash算法分割开来,来提升性能。

思路转化方案:

  1. 涉及的Redis缓存
  • 缓存1(hash),用于找到缓存2

大Key:device:one:0,小Key:pk:8620241008283980,Value:device:two:202410091900, 即 {"pk:8620241008283980":"device:two:202410091900"}

  • 缓存2(hash), 通过缓存2达到过滤数据的目的

大Key:device:two:202410091900,小Key:pk:8620241008283980,Value:1728473450149, 即 {"pk:8620241008283980":"1728473450149"}

  1. PK:DK按照hash算法,分成100份,设备上报时,存储到缓存1中
  2. 按照1分钟为跨度,设备上报时,将当前设备数据存储到缓存2中
  3. 设备上报时,判断该设备是否有延迟定时任务,如果存在删除该延迟定时任务,判断该设备是否存在缓存1与缓存2,如果存在先删除,再添加。(其过程实现了数据在缓存2不同集合的转化)
  4. 定时任务:根据当前时间,扫描对应60/50/40min前的缓存2数据,并添加到延迟定时任务(考虑到消息要及时发送)中
  5. 延迟定时任执行:删除缓存1该设备数据,删除缓存2该设备数据,下发通知

基本流程

方案示意图:
在这里插入图片描述
设备上报处理流程:
在这里插入图片描述
定时任务处理流程:
在这里插入图片描述

业务流程实现

设备上报处理逻辑

场景1: 缓存1中存在,缓存2中也存在,延迟定时任务中也存在

删除该设备延迟定时任务数据,删除缓存2数据,删除缓存1数据,新增缓存2,新增缓存1

场景2: 缓存1中存在,缓存2中也存在,延迟定时任务中不存在

删除缓存2数据,删除缓存1数据,新增缓存2,新增缓存1

场景3: 缓存1不存在,缓存2中不存在,延迟定时任务中不存在

新增缓存2,新增缓存1

相关代码:

package com.angel.ocean.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.angel.ocean.redis.RedisCacheKey;
import com.angel.ocean.util.FutureTaskUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Map;

@Slf4j
@Service
public class DataHandlerService {

    private static final String COMMA = ":";

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private ScheduleTaskService scheduleTaskService;

    public void setCache(String productKey, String deviceKey, long ts, int expiredNoticeTime) {

        String childKey = productKey + COMMA + deviceKey;

        String oneKey = RedisCacheKey.getCacheOneHashKey(productKey, deviceKey);
        RMap<String, String> oneHash = redissonClient.getMap(oneKey);

        String oldTwoKey = oneHash.get(childKey);
        if(StrUtil.isNotEmpty(oldTwoKey)) {

            if(FutureTaskUtil.futureTasks.containsKey(childKey)) {
                log.info("移除通知延迟任务,{}", childKey);
                scheduleTaskService.stopTask(childKey);
            }

            RMap<String, String> oldTwoHash = redissonClient.getMap(oldTwoKey);
            log.info("该设备缓存已存在,先删除历史缓存,再更新,{}", childKey);

            // 删除缓存2
            oldTwoHash.remove(childKey);
            // 删除缓存1
            oneHash.remove(childKey);
        }

        String twoKey = RedisCacheKey.getCacheTwoHashKey(ts);
        RMap<String, String> twoHash = redissonClient.getMap(twoKey);
        long expiredTime = ts + expiredNoticeTime * 60 * 1000L;

        twoHash.put(childKey, Long.toString(expiredTime));
        oneHash.put(childKey, twoKey);
    }
}

缓存工具类:

package com.angel.ocean.redis;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
 *  缓存键
 */
public class RedisCacheKey {

    public static final String COMMA = ":";

    private static final int n = 100;

    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmm");

    /**
     * 获取缓存1 Key,依据pk和dk
     * @param productKey
     * @param deviceKey
     * @return
     */
    public static String getCacheOneHashKey(String productKey, String deviceKey) {
        String data = productKey + COMMA + deviceKey;
        return "device:one:" + Math.abs(data.hashCode()) % n;
    }

    /**
     * 获取缓存2 Key,依据时间戳
     * @param ts
     * @return
     */
    public static String getCacheTwoHashKey(long ts) {

        // 将时间戳转换为 Instant
        Instant instant = Instant.ofEpochMilli(ts);
        ZoneId zoneId = ZoneId.systemDefault();
        // 转换为 ZonedDateTime
        ZonedDateTime zdt = instant.atZone(zoneId);

        // 格式化 ZonedDateTime
        String formattedDateTime = zdt.format(formatter);

        // 构建并返回缓存键
        return "device:two:" + formattedDateTime;
    }

    public static void main(String[] args) {
        System.out.println(getCacheTwoHashKey(System.currentTimeMillis()));
    }
}

定时任务逻辑(每分钟执行一次)

  • 依据当前时间和多久未上报(60/50/40min),获取对应的缓存2数据
  • 遍历该缓存2集合
  • 判断该设备的通知时间,是否小于当前时间加上1分钟,如果小于就加入到延迟定时任务中
  • 延迟定时任务执行时,删除该设备的缓存2数据,删除该设备的缓存1数据

相关代码:

package com.angel.ocean.task;

import com.angel.ocean.service.DataHandlerService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;

@Slf4j
@Component
public class ScheduledTasks {

    @Resource
    private DataHandlerService dataHandlerService;

    // 每1分钟执行一次
    // 遍历缓存2,放入延迟定时任务中
    @Scheduled(cron = "0 0/1 * * * ?")
    public void dataHandler() {

        log.info("dataHandler....");

        // 60分钟未上报通知
        dataHandlerService.delayTaskHandler(60);

        // 50分钟未上报通知
        dataHandlerService.delayTaskHandler(50);

        // 40分钟未上报通知
        dataHandlerService.delayTaskHandler(40);
    }
}
package com.angel.ocean.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.angel.ocean.redis.RedisCacheKey;
import com.angel.ocean.util.FutureTaskUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Map;

@Slf4j
@Service
public class DataHandlerService {

    private static final String COMMA = ":";

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private ScheduleTaskService scheduleTaskService;
    
    // 将数据放入延迟定时任务
    public void delayTaskHandler(int delayTime) {

        long start = System.currentTimeMillis();
        log.info("delayTaskHandler() start..., time:{}", System.currentTimeMillis());

        long now = System.currentTimeMillis();

        long ts = now - delayTime * 60 * 1000L;

        String twoKey = RedisCacheKey.getCacheTwoHashKey(ts);
        RMap<String, String> hashMap = redissonClient.getMap(twoKey);

        if(CollUtil.isEmpty(hashMap)) {
            return;
        }

        Map<String, String> allEntries = hashMap.readAllMap();

        allEntries.forEach((key, value) -> {
            long tsLimit = now + 60000;
            log.info("tsLimit={}, ts={}", tsLimit, value);
            if(Long.parseLong(value) < tsLimit) {

                Runnable task = () -> {
                    noticeHandler(key, twoKey);
                };

                if(Long.parseLong(value) <= System.currentTimeMillis()) {
                    scheduleTaskService.singleTask(task);
                } else {
                    scheduleTaskService.delayTask(key, task, Long.parseLong(value) - System.currentTimeMillis());
                }
            }
        });

        long end = System.currentTimeMillis();
        log.info("delayTaskHandler() end..., 耗时:{}毫秒", (end - start));
    }

    // 模拟通知逻辑
    private void noticeHandler(String childKey, String twoKey) {

        log.info("发送通知,设备:{}, ts={}",  childKey, System.currentTimeMillis());

        String[] arr = childKey.split(RedisCacheKey.COMMA);
        String oneKey = RedisCacheKey.getCacheOneHashKey(arr[0], arr[1]);
        RMap<String, String> oneHash = redissonClient.getMap(oneKey);
        String currentTwoKey = oneHash.get(childKey);

        // 由于并发问题,会存在延迟定时任务(twoKey)的与缓存1中存储的值(currentTwoKey)不一致,因此,需要校验两个值是否相同。
        if(StrUtil.isNotEmpty(currentTwoKey) && currentTwoKey.equals(twoKey)) {
            // TODO 相同的话执行通知逻辑,删除缓存1
            // 删除缓存1
            oneHash.remove(childKey);
        }

        // 删除缓存2,无论twoKey与currentTwoKey相不相同都删除
        RMap<String, String> twoHash = redissonClient.getMap(twoKey);
        twoHash.remove(childKey);
    }
}

延迟定时任务实现

Springboot定时任务,线程池配置

package com.angel.ocean.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
public class SchedulerConfig {

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(20); // 设置线程池大小
        scheduler.setThreadNamePrefix("Thread-task-"); // 设置线程名称前缀
        scheduler.setDaemon(true); // 设置为守护线程

        // 你可以继续设置其他属性...
        return scheduler;
    }
}

定时任务工具类

package com.angel.ocean.util;

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;

@Slf4j
public class FutureTaskUtil {

    private FutureTaskUtil() {
    }

    // FutureTask集合
    public static ConcurrentMap<String, ScheduledFuture<?>> futureTasks = new ConcurrentHashMap<String, ScheduledFuture<?>>();

    /**
     * 判断是否包含 futureTask
     * @param taskId
     * @return
     */
    public static boolean isContains(String taskId) {

        boolean result = false;
        if(futureTasks.containsKey(taskId)) {
            result = true;
        }

        return result;
    }

    /**
     * 添加 futureTask
     * @param taskId
     * @param futureTask
     */
    public static void addFutureTask(String taskId, ScheduledFuture<?> futureTask) {
        if(futureTasks.containsKey(taskId)) {
            log.error("FutureTaskUtil.addFutureTask(), key: {}已存在", taskId);
            return;
        }
        futureTasks.put(taskId, futureTask);
    }

    /**
     * 获取 futureTask
     * @param taskId
     * @return
     */
    public static ScheduledFuture<?> getFutureTask(String taskId) {

        ScheduledFuture<?> futureTask = null;

        if(futureTasks.containsKey(taskId)) {
            log.info("FutureTaskUtil.getFutureTask(), taskId: {}", taskId);
            futureTask = futureTasks.get(taskId);
        }

        return futureTask;
    }

    /**
     * 移除 futureTask
     * @param taskId
     */
    public static void removeFutureTask(String taskId) {
        if(futureTasks.containsKey(taskId)) {
            log.info("FutureTaskUtil.removeFutureTask(), taskId: {}", taskId);
            futureTasks.remove(taskId);
        }
    }
}

需要关注的问题

  • 并发问题如何处理?

由于并发问题,会造成缓存1和缓存2的数据不一致,延迟任务执行时校验缓存1中存储的缓存2的Key于延迟定时任务的缓存Key是否一致,一致的话才下发通知。

  • 服务重启,造成延迟定时任务数据丢失,如何补发通知?

由于延迟定时任务存在于内存中,服务重新启动,会导致其数据丢失,可以考虑从缓存2再拿一次数据,做个数据补偿。

package com.angel.ocean.runner;

import com.angel.ocean.service.DataHandlerService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;

@Slf4j
@Component
public class StartupRunner implements CommandLineRunner {

    @Resource
    private DataHandlerService dataHandlerService;

    @Override
    public void run(String... args) throws Exception {

        // 只处理近2个小时的数据
        int i = 120;
        while (i > 50) {
            dataHandlerService.delayTaskHandler(i);
            i = i - 1;
        }
    }
}

模拟验证

package com.angel.ocean.test;

import cn.hutool.core.util.RandomUtil;
import com.angel.ocean.domain.DeviceCacheInfo;
import java.util.ArrayList;
import java.util.List;

public class DeviceDataUtil {

    private static int deviceNumber = 500000;
    private static List<String> dks = new ArrayList<>(deviceNumber + 5);
    private static boolean initFlag = false;

    private static void init() {
        int number = 1;
        while (number <= deviceNumber) {
            String formattedNumber = String.format("%06d", number);
            String dk = "8620241008" + formattedNumber;
            dks.add(dk);
            number++;
        }

        initFlag = true;
    }

    public static void setDeviceNumber(int number) {
        DeviceDataUtil.deviceNumber = number;
    }

    public static DeviceCacheInfo deviceReport() {

        if(!initFlag) {
            init();
        }

        DeviceCacheInfo deviceCacheInfo = new DeviceCacheInfo();
        deviceCacheInfo.setProductKey("pk");
        deviceCacheInfo.setTs(System.currentTimeMillis());
        deviceCacheInfo.setExpiredNoticeTime(60);
        String dk = dks.get(RandomUtil.randomInt(1, deviceNumber));
        deviceCacheInfo.setDeviceKey(dk);

        return deviceCacheInfo;
    }
}
package com.angel.ocean;

import com.angel.ocean.domain.DeviceCacheInfo;
import com.angel.ocean.service.DataHandlerService;
import com.angel.ocean.test.DeviceDataUtil;
import com.angel.ocean.util.ThreadPoolUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;

@Slf4j
@SpringBootTest
class ApplicationTests {

    @Resource
    private DataHandlerService dataHandlerService;

    @Test
    void contextLoads() {
        for(int i = 0; i < 500000; i++) {
            DeviceCacheInfo deviceCacheInfo = DeviceDataUtil.deviceReport();
            Runnable task = () -> {
                dataHandlerService.setCache(deviceCacheInfo.getProductKey(), deviceCacheInfo.getDeviceKey(), deviceCacheInfo.getTs(), deviceCacheInfo.getExpiredNoticeTime());
            };
            ThreadPoolUtil.pools.submit(task);
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
package com.angel.ocean.util;

import cn.hutool.core.thread.ThreadFactoryBuilder;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolUtil {

    private ThreadPoolUtil() {}

    public static final ThreadPoolExecutor pools = new ThreadPoolExecutor(16, 50, 60, TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(10000),
            new ThreadFactoryBuilder().setNamePrefix("MyThread-").build(),
            new ThreadPoolExecutor.CallerRunsPolicy());
}

缓存截图:
在这里插入图片描述
缓存1:
在这里插入图片描述缓存2:
在这里插入图片描述运行日志截图:
在这里插入图片描述执行延迟定时任务日志截图:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2201815.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

叶国富的永辉填坑之旅

叶国富体验了一把过山车&#xff01;永辉的难题逐渐转移到名创优品&#xff0c;后者是否能应对这些问题&#xff0c;以及其股价的徘徊&#xff0c;都预示着挑战才刚刚开始。 转载&#xff1a;原创新熵 作者丨樱木 编辑丨蕨影 低迷了3年的二级市场&#xff0c;迎来了超级反转&…

【金九银十】笔试通关 + 小学生都能学会的堆排序

算法原理 堆排序是一种基于比较的排序算法&#xff0c;它利用了数据结构中的堆&#xff08;Heap&#xff09;。堆是一种特殊的完全二叉树&#xff0c;分为最大堆&#xff08;Max-Heap&#xff09;和最小堆&#xff08;Min-Heap&#xff09;。在最大堆中&#xff0c;每个父节点…

单场数字人直播爆量300万,GMV狂增80%,电商人如何玩转数字人直播?

单场直播带货300万&#xff0c;在头部主播那里也许不算什么。但如果告诉你&#xff0c;这是数字人直播做出的成绩&#xff0c;你会惊讶吗&#xff1f; 苏宁借力电商数字人开播&#xff0c;直播时长比以往能增加3倍&#xff0c;GMV增量80%&#xff0c;下单转化57%&#xff0c;不…

通过祖先序列重建辅助工程化UDP-糖基转移酶-文献精读64

Engineering the Substrate Specificity of UDP-Glycosyltransferases for Synthesizing Triterpenoid Glycosides with a Linear Trisaccharide as Aided by Ancestral Sequence Reconstruction 通过祖先序列重建辅助工程化UDP-糖基转移酶的底物特异性&#xff0c;用于合成具…

RWKV-CHN模型部署教程

一、模型介绍 RWKV 语言模型&#xff08;用纯 100%RNN 达到 GPT 能力&#xff0c;甚至更强&#xff09;&#xff0c;该项目旨在通过为您自动化所有事情来消除使用大型语言模型的障碍。您需要的是一个只有几兆字节的轻量级可执行程序。此外&#xff0c;该项目还提供了一个接口兼…

Vue打印网页pdf,并且有按钮调整缩小放大

本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:王文峰…

[AutoSar]BSW_Diagnostic_005 RoutineControl service (0x31)介绍

目录 关键词平台说明背景一、请求格式二、sub-function definition三、响应格式四、NRC五、case 关键词 嵌入式、C语言、autosar、OS、BSW、UDS、diagnostic 平台说明 项目ValueOSautosar OSautosar厂商vector芯片厂商TI编程语言C&#xff0c;C编译器HighTec (GCC)autosar版…

录屏工具分享

遇到问题 现在很多录屏工具都是要会员 要么就不清&#xff0c;压缩画质 解决方案 &#xff08;1&#xff09;QQ录屏 QQ录屏缺点就是界面上会有个录屏计时阻挡。没有影响的话可以使用。录几分钟出来也是几百M的容量 &#xff08;2&#xff09;格式工厂 录的视频很清晰&…

打造梦幻AI开发环境:一步步解锁高效配置的魅力

作者简介&#xff1a;我是团团儿&#xff0c;是一名专注于云计算领域的专业创作者&#xff0c;感谢大家的关注 座右铭&#xff1a; 云端筑梦&#xff0c;数据为翼&#xff0c;探索无限可能&#xff0c;引领云计算新纪元 个人主页&#xff1a;团儿.-CSDN博客 目录 前言&#…

【如何保存Pixso中原型图的图标】

【如何保存Pixso中原型图的图标】 在软件UI设计完成后&#xff0c;设计师需要将设计中的图标导出为开发团队所需的图片文件&#xff0c;以便进行后续开发工作。pixso中原型图的图标到处如下图&#xff0c;按照序号操作流程即可到处图片。

wireshark获取QQ图片

今天随手写下之前做的一个比较有意思的实验&#xff1a;Wireshark抓取qq图片 1.前提 手机和电脑处于同一网络之中&#xff0c;这里我使用了校园网。 接着使用手机向电脑发出图片 2.wireshark流量抓包 先查看好手机的ip地址&#xff0c;随后使用命令&#xff1a;ip.src10.33.X…

MybatisPlus的日常使用

一、基础接口 public interface BaseMapper<T> {/*** 插入一条记录* param entity 实体对象*/int insert(T entity);/*** 根据 ID 删除* param id 主键ID*/int deleteById(Serializable id);/*** 根据 columnMap 条件&#xff0c;删除记录* param columnMap 表字段 map …

Anthropic Message Batches API 满足批量处理大量请求

现在开发的系统有大量知识汇总统计、跑批处理需求的同学可以尝试一下&#xff0c;看看能不能解决自己目前的问题~~ 可能是一个解决方案 Anthropic 推出的 Message Batches API &#xff0c;专门用于帮助开发者批量处理大量请求。它的主要目的是通过一次性处理大量非实时任务&a…

Linux工具的使用——【gcc/g++的使用】【make/Makefile的使用】【如何让普通用户使用sudo】

目录 Linux工具的使用-021.如何让普通用户使用sudo1.1为什么无法使用sudo1.2解决步骤1.3验证 2.编译器gcc/g的使用2.1预处理2.2编译2.3汇编2.4链接2.5函数库2.5.1静态库与动态库2.5.1.1动态链接2.5.1.2静态链接 2.6gcc的默认链接方式2.7gcc的静态链接2.8g的使用2.8.1g的静态链接…

Apache Flink Dashboard

1、Overview Apache Flink Web Dashboardhttp://110.40.130.231:8081/#/overview 这张图片显示的是Apache Flink的Web UI界面&#xff0c;其中包含了以下几个部分&#xff1a; Available Task Slots: 显示当前可用的任务槽位数量。任务槽位是指Flink集群中可用于运行任务的资…

【华为HCIP实战课程十】OSPF网络DR和BDR实战讲解,网络工程师

一、DR与BDR的基础介绍 点到点同步LSA成本小 多点接入网络同步LSA成本大,需要DR/BDR 由于MA网络中,任意两台路由器都需要传递路由信息,网络中有n台路由器,则需要建立n*(n-1)/2个邻接关系。任何一台路由器的路由变化都会导致多次传递,浪费了带宽资源,DR和BDR应运而生!…

大数据存储计算平台EasyMR:多集群统一管理助力企业高效运维

随着全球企业进入数字化转型的快车道&#xff0c;数据已成为企业运营、决策和增长的核心驱动力。为了处理海量数据&#xff0c;同时应对数据处理的复杂性和确保系统的高可用性&#xff0c;企业往往选择部署多个Hadoop集群&#xff0c;这样的策略可以将生产环境、测试环境和灾备…

分布式 ID

背景 在复杂分布式系统中&#xff0c;往往需要对大量的数据和消息进行唯一标识。随着数据日渐增长&#xff0c;对数据分库分表后也需要有一个唯一ID来标识一条数据或消息&#xff0c;数据库的自增 ID 显然不能满足需求&#xff1b;此时一个能够生成全局唯一 ID 的系统是非常必…

电商选品/跟卖| 亚马逊卖家精灵爬虫

卖家精灵(SellerSprite)基于大数据和人工智能技术,精准查询每个产品的销量、关键词、自然搜索数据,为亚马逊跨境卖家提供一站式选品、市场分析、关键词优化、产品监控等, 基于买家精灵跟卖,可谓事半功倍, 如何利用买家精灵, 快速获取跟卖信息. from extensions.basic_exte…

Java基础知识——String篇

一、String 1、是什么 String 是 Java 中用于表示字符串的类。Java 中的字符串是不可变的&#xff0c;也就是说一旦创建&#xff0c;字符串的内容无法更改。 2、如何构造 &#xff08;1&#xff09;无参数构造方法&#xff1a; String str new String(); //创建一个空字符…