分布式id的要求

  • 唯一性:可以在多个系统之间保持唯一性。
  • 高性能:redis是基于内存的数据库,性能极高。
  • 高可用:redis支持集群。
  • 递增性:具有单调递增的特性
  • 安全性:id拼接了其他信息,递增的规律性不会太明显。

基于redis的id的组成

id由三个部分组成,符号位,时间戳,序列号。

使用long类型存储,长整型占八个字节,1个字节等于8个比特位,即64位。

第1位表示符号位,永远是0;

第2~32位表示时间戳,以秒为单位,可以使用69年;

剩下的33~64位表示序列号,秒内的计数器,支持每秒产生2^32个不同的id;

代码实现

1、生成时间戳

  1. 需要事先准备一个固定的起始时间戳,先运行main方法里面的代码获得。
  2. 每次生成id时,获取当前时间戳。通过“起始时间戳”减去“当前时间戳”,获得组成id的时间戳。
// 1、
private static final long BEGIN_TIMESTAMP = 1672531200L;
public static void main(String[] args){
    // 获得一个时间戳,时间为2023年1月1日0点0分0秒
    LocalDateTime time = LocalDateTime.of(2023, 1, 1, 0, 0, 0);
    long timestamp = time.toEpochSecond(ZoneOffset.UTC);
    System.out.println(timestamp);  // 1672531200
}

// 2、
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond = BEGIN_TIMESTAMP;

2、生成序列号

其实就是在redis里设置一个key,通过对key自增,每次获得id使用自增后的值,组成序列号部分。

需要特别注意key的组成,因为redis中key的自增上限是2^64次方,虽然已经很大了,但毕竟还是有上限。所以解决办法就是“key+日期”。

这里将key拼接上生成id时的年月日,这样做的好处是,key会随着年月日变化;还有利于统计,某天的key自增的值就代表当前生成的id的数量。

/**
*    “icr”:自增key的标识,可改
*    “keyPrefix”:业务id
*    “date”:当前年月日
*
*    例如:icr:order:2023:07:28
*/
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long count = stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);

3、组成id

long类型是占64个bit,此时将其左移32位,原来的32个位置上会自动补0。然后再将结果和序列号进行“或运算”,就得到了完整id。

long id = timestamp << 32 | count;
return id;

4、图解:

原始时间戳
时间戳1.png

左移32位的时间戳
时间戳2.png

或运算之后的时间戳
时间戳3.png

此时id的前32位是符号位和时间戳组成,后32位是存储在redis里的那个自增key的值。id会随时间戳而变化,而时间戳是精确到秒,所以理论上,就算在极高的并发下,只要一秒内并发不超过2^32次方,id就是唯一的。

完整代码

@Component
public class RedisIdWorker {
    // 开始时间戳
    private static final long BEGIN_TIMESTAMP = 1672531200L;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextID(String keyPrefix){
        // 1、生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond = BEGIN_TIMESTAMP;

        // 2、生成序列号
        // 获取当前日期,精确到天。用以解决redis中,单key自增上限问题,还有利于统计。redis单key自增上限为2^64次方
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);

        // 3、拼接并返回
            //生成的时间戳左移32位,然后或上自增长的值
        long id = timestamp << 32 | count;
        return id;
    }
}

性能测试

用线程池模拟并发,for循环提交任务。

    private ExecutorService es = Executors.newFixedThreadPool(500);
    @Test
    void testIdWorker() throws InterruptedException {
        CountDownLatch cdl = new CountDownLatch(500);
        
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextID("order");
                System.out.println(id);
            }
            cdl.countDown();
        };
        
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        for (int i = 0; i < 500; i++) {
            es.submit(task);
        }
        
        cdl.await();
        stopWatch.stop();
        
        // time = 3961
        System.out.println("time = " + stopWatch.getTotalTimeMillis());
    }

本地电脑测试结果:5万个id,用时3秒多。

本次测试环境基于JDK1.8、SpringBoot,依赖由Maven管理。

本文章对腾讯云官方的api文档进行了测试,且略有改进,并适当添加注释。

1、环境配置

  • 腾讯云存储对应的JDK文档页面

https://cloud.tencent.com/document/product/436/10199

  • 先引入API相关依赖

    <dependency>
        <groupId>com.qcloud</groupId>
        <artifactId>cos_api</artifactId>
        <version>5.6.97</version>
    </dependency>
  • 在resource资源目录下配置相关文件,存储腾讯云密钥和云存储的基本信息

config.properties

### 腾讯云
# 密钥
qcloud.secretId=AKID********xtSLn5XWDI86bm
qcloud.secretKey=d1I********1vd6
# cos配置
# region,桶存在的区域
qcloud.region=ap-shanghai    
# bucketName,即桶的名字
qcloud.bucketName=test-1200000000
# 访问域名
qcloud.Domain=https://test-1200000000.cos.ap-shanghai.myqcloud.com/
  • 在config类里配置相关Bean

    @Configuration
    public class Configure {
    
        /**
         * 该Bean用于读取资源目录下的config.properties文件
         */
        @Bean("cosConfig")
        public Properties cosConfig(){
            Properties properties = new Properties();
            try(InputStream is = ClassLoader.getSystemResourceAsStream("config.properties")) {
                properties.load(is);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            return properties;
        }
    
        /**
         * 注册一个配置好的COS客户端
         */
        @Bean("cosClient")
        public COSClient createCOSClient(){
            // 读取配置文件获取密钥
            Properties properties = cosConfig();
            String secretId = properties.getProperty("qcloud.secretId");
            String secretKey = properties.getProperty("qcloud.secretKey");
            String sessionToken = "TOKEN";
    
            // 传入密钥
            BasicSessionCredentials credentials = new BasicSessionCredentials(secretId, secretKey, sessionToken);
            COSCredentials cosCredentials = new BasicCOSCredentials(secretId, secretKey);
    
            // 设置bucket的地域
            Region region = new Region(properties.getProperty("qcloud.region"));
            ClientConfig clientConfig = new ClientConfig(region);
            // 设置https
            clientConfig.setHttpProtocol(HttpProtocol.https);
    
            // 可选:设置socket读取超时,默认30s
            clientConfig.setSocketTimeout(5*1000);
            // 可选:设置建立连接超时时间,默认30s
            clientConfig.setConnectionTimeout(5*1000);
    
            // 如果需要的话,设置 http 代理,ip 以及 port
            //clientConfig.setHttpProxyIp("httpProxyIp");
            //clientConfig.setHttpProxyPort(80);
    
            // 生成cos客户端
            COSClient cosClient = new COSClient(cosCredentials, clientConfig);
            return cosClient;
        }
    }

云存储的增删改查皆由COSClient 类来发起请求和接收接口。

根据腾讯云的文档可知:

  1. COSClient 是线程安全的类,允许多线程访问同一实例。
    因为实例内部维持了一个连接池,创建多个实例可能导致程序资源耗尽。请确保程序生命周期内实例只有一个,且在不再需要使用时,调用 COSClient.shutdown() 方法将其关闭。

如果需要新建实例,请先将之前的实例关闭。推荐一个进程里只使用一个 COSClient,在程序全部结束退出时才调用 COSClient.shutdown()

所以将COSClient 交给spring管理刚刚好,因为spring管理的Bean默认是单实例的。

2、上传文件

  1. 文件上传:

对象存储中本身没有文件夹和目录的概念,文件的完整路径用Key和Value表示,例如有一个文件的完整路径是/2023/04/11/1.png,此时,“/2023/04/11/”是Key,“1.png”是Value。

    @Resource
    public Properties cosConfig;

    @Resource
    private COSClient cosClient;

    /**
     * 使用简单接口上传,文件类型
     */
    @Test
    public void cosClientUploadFile(){
        String bucketName = cosConfig.getProperty("qcloud.bucketName");
        File file = new File("1.webp");
        String KV = "/test/" + file.getName();

        /**
         * 参数1:桶名称
         * 参数2:上传后存放的完整路径。完整路径+文件名
         * 参数3:要上传的文件
         */
        // PutObjectRequest 用于设置请求信息
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, KV, file);
        try{
            // 如果这里没有发生异常,则表示本次请求操作成功。
            // PutObjectResult 类用于返回结果信息
            PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
            System.out.println(putObjectResult.getRequestId());
        } catch (CosServiceException e) {
            e.printStackTrace();
        } catch (CosClientException e) {
            e.printStackTrace();
        }
        // 确认本进程不再使用 cosClient 实例之后,关闭之
        cosClient.shutdown();
    }
  1. 流上传:

    /**
     * 使用简单接口上传,流类型
     * 上传的源是一个 InputStream 类型(和其子类型)的流实例。
     */
    @Test
    public void cosClientUploadStream(){
        String bucketName = cosConfig.getProperty("qcloud.bucketName");
        File file = new File("1.webp");
        String KV = "test/" + file.getName();
        try(FileInputStream fis = new FileInputStream(file)) {
            // ObjectMetadata 类用于记录对象的元信息
             ObjectMetadata objectMetadata = new ObjectMetadata();
    
             PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, KV, fis, objectMetadata);
             
            PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
            System.out.println(putObjectResult.getRequestId());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 确认本进程不再使用 cosClient 实例之后,关闭之
        cosClient.shutdown();
    }

3、下载文件

  • 从指定桶下载文件

    @Test
    public void cosLoadTest(){
      String bucketName = cosConfig.getProperty("qcloud.bucketName");
      /**
       * 创建一个get请求
       * 参数1:桶名称
       * 参数2:文件的key
       */
      GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, "test/2.png");
      File downloadFile = new File("11.jpg");
      try {
          /**
           * 返回一个异步结果 Download, 可同步的调用 waitForCompletion 等待下载结束, 成功返回 void, 失败抛出异常
           * 下载的文件回存入downloadFile中
           */
          Download download = transferManager.download(getObjectRequest, downloadFile);
          download.waitForCompletion();
      } catch (CosServiceException e) {
          e.printStackTrace();
      } catch (CosClientException e) {
          e.printStackTrace();
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
    
      transferManager.shutdownNow(true);
    }

4、删除文件

  • 从指定桶删除文件

    @Test
    public void deleteCOSFile(){
      String bucketName = cosConfig.getProperty("qcloud.bucketName");
      String key = "test/1.jpg";
      cosClient.deleteObject(bucketName, key);
    }

5、查询文件

只有批量顺序查询,没有单个查询

  • 单次批量查询

官方的说法是列出第一页对象,第一页的数量可以设置,但是最大1000.

很简陋,且查出来的数据是无序的。

    @Test
    public void queryList1(){
        String bucketName = cosConfig.getProperty("qcloud.bucketName");

        // new一个专门的请求对象
        ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
        // 设置bucketName名称
        listObjectsRequest.setBucketName(bucketName);
        // 设置列出的对象名以prefix为前缀,可找出文件名前缀相同的对象
        listObjectsRequest.setPrefix("");
        // 设置最大列出多少个对象,一次listObject最大支持1000
        listObjectsRequest.setMaxKeys(10);

        /**
         * 发起请求,并保存列出的结果
         *      成功:返回 ObjectListing 类型, 包含所有的成员, 以及 nextMarker,是上一批列表中的最后一个对象的名字
         *      失败:抛出异常 CosClientException 或者 CosServiceException。详情请参见 异常处理。
         */
        ObjectListing objectListing = null;
        try {
            objectListing = cosClient.listObjects(listObjectsRequest);
        }catch (CosServiceException e){
            e.printStackTrace();
        }catch (CosClientException e){
            e.printStackTrace();
        }

        // object summary表示此次列出的对象列表
        List<COSObjectSummary> cosObjectSummaries = objectListing.getObjectSummaries();
        System.out.println("本次列出数据条数为:" + cosObjectSummaries.size());
        for (COSObjectSummary cosObjectSummary : cosObjectSummaries) {
            // 对象的 key
            String key = cosObjectSummary.getKey();
            // 对象的 etag
            String etag = cosObjectSummary.getETag();
            // 对象的长度
            long fileSize = cosObjectSummary.getSize();
            // 对象的存储类型
            String storageClasses = cosObjectSummary.getStorageClass();
            System.out.println(cosConfig.getProperty("qcloud.Domain") + key + ", " + etag + ", " + fileSize + ", " + storageClasses);
        }

        cosClient.shutdown();
    }
  • 多次批量查询

同样简陋,查出来的数据也是无序的。但是可以循环查询全部数据。

代码解读:

​ 请求流程都在do-while里完成,即先查一遍,然后再判断是否未查完。如未查完则携带标记继续循环查询。

​ 由客户端cosClient发起的查询请求会返回一个列表对象objectListing,该对象有一个方法objectListing.isTruncated()可以判断本次查询的列表的后面是否还有数据,如果有,该方法会返回true,然后用objectListing.getNextMarker()方法可以获取下一次标记,其实就是本次结果列表的最后一条数据的key,然后将这个key标记设置到请求对象里面,下次查询复用这个对象即可。

​ 下次开始发送请求时,listObjectsRequest对象将会携带上一次标记,然后该次请求就会从该标记位置开始返回数据,如此循环往复,就可以查完全部数据。

    @Test
    public void queryList2(){
        String bucketName = cosConfig.getProperty("qcloud.bucketName");

        boolean flag = false;
        String netMarker = "";
        do {
            // 每次循环开始时,先设置循环标记为false,等本次请求发现没有列完时,再设置回true,让while继续循环。
            flag = false;

            ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
            // 设置bucketName名称
            listObjectsRequest.setBucketName(bucketName);
            // 设置列出的对象名以prefix为前缀
            listObjectsRequest.setPrefix("");
            // 设置最大列出多少个对象,一次listObject最大支持1000
            listObjectsRequest.setMaxKeys(10);
            // 设置标记位置
            listObjectsRequest.setMarker(netMarker);

            ObjectListing objectListing = null;
            try {
                objectListing = cosClient.listObjects(listObjectsRequest);
            }catch (CosServiceException e){
                e.printStackTrace();
            }catch (CosClientException e){
                e.printStackTrace();
            }
            // 打印出结果列表里的对象
            List<COSObjectSummary> cosObjectSummaries = objectListing.getObjectSummaries();
            System.out.println("本次列出数据条数为:" + cosObjectSummaries.size());
            for (COSObjectSummary cosObjectSummary : cosObjectSummaries) {
                // 对象的 key
                String key = cosObjectSummary.getKey();
                // 对象的 etag
                String etag = cosObjectSummary.getETag();
                // 对象的长度
                long fileSize = cosObjectSummary.getSize();
                // 对象的存储类型
                String storageClasses = cosObjectSummary.getStorageClass();
                System.out.println(cosConfig.getProperty("qcloud.Domain") + key + ", " + etag + ", " + fileSize + ", " + storageClasses);
            }
            // 判断是否查完
            if (objectListing.isTruncated()){
                // 表示还没有列完,被截断了
                // 这里的返回值是一个字符串,具体是本次结果集的最后一条数据的key
                 netMarker = objectListing.getNextMarker();
                System.out.println(netMarker);
                flag = true;
            }
        }while (flag);

        cosClient.shutdown();
    }

6、列出指定目录下的目录及文件

​ 只能查到下面一级,如果想查深一点,可以获取到目录的时候再递归查询目录

    @Test
    public void queryList3(){
        String bucketName = cosConfig.getProperty("qcloud.bucketName");

        ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
        // 设置bucketName
        listObjectsRequest.setBucketName(bucketName);
        // 这里填要列出的目录的相对 bucket 的路径
        listObjectsRequest.setPrefix("/test/");
        // delimiter 表示目录的截断符, 例如:设置为 / 则表示对象名遇到 / 就当做一级目录)
        listObjectsRequest.setDelimiter("/");
        // 设置最大遍历出多少个对象, 一次 listobject 最大支持1000
        listObjectsRequest.setMaxKeys(100);

        // 保存每次列出的结果
        ObjectListing objectListing = null;
        do {
            try{
                // 发起请求
             objectListing = cosClient.listObjects(listObjectsRequest);
            } catch (CosServiceException e) {
                e.printStackTrace();
                return;
            } catch (CosClientException e) {
                e.printStackTrace();
                return;
            }

            // 这里保存列出来的子目录
            List<String> commonPrefixes = objectListing.getCommonPrefixes();
            System.out.println("目录");
            for (String commonPrefix : commonPrefixes) {
                System.out.println(commonPrefix);
            }

            // 这里保存列出的对象列表
            List<COSObjectSummary> cosObjectSummaries = objectListing.getObjectSummaries();
            System.out.println("对象");
            for (COSObjectSummary cosObjectSummary : cosObjectSummaries) {
                // 对象的 key
                String key = cosObjectSummary.getKey();
                System.out.println(key);
            }

            // 标记下一次开始的位置
            String nextMarker = objectListing.getNextMarker();
            listObjectsRequest.setMarker(nextMarker);
        }while (objectListing.isTruncated());
    }

7、判断文件是否存在

    @Test
    public void isExistFile(){
        String bucketName = cosConfig.getProperty("qcloud.bucketName");
        String key = "/test/2.png";

        try {
            boolean result = cosClient.doesObjectExist(bucketName, key);
            if (result){
                System.out.println("存在");
            }else {
                System.out.println("不存在");
            }
        }catch (CosServiceException e){
            e.printStackTrace();
        }catch (CosClientException e){
            e.printStackTrace();
        }
    }

8、查询对象的元数据

   @Test
    public void queryObjectMetaData(){
        String bucketName = cosConfig.getProperty("qcloud.bucketName");
        String key = "/test/2.png";

        try {
            ObjectMetadata objectMetadata = cosClient.getObjectMetadata(bucketName, key);
            System.out.println(objectMetadata.getCrc64Ecma());
            System.out.println(objectMetadata.getLastModified());
            System.out.println(objectMetadata.getETag());
            System.out.println(objectMetadata.getRequestId());
        }catch (CosServiceException e){
            e.printStackTrace();
        }catch (CosClientException e){
            e.printStackTrace();
        }

        cosClient.shutdown();
    }

9、修改对象元数据

    /**
     * 修改对象元数据
     * 修改对象元数据利用了复制对象的接口,在复制过程中设置新的元数据。
     * 使用复制对象接口,在复制过程中设置新的元数据。在复制接口中仅仅修改元数据,不会执行对象数据的复制。
     */
    @Test
    public void changeObjectMetaData(){
        String bucketName = cosConfig.getProperty("qcloud.bucketName");
        String key = "/test/2.png";

        // 获取当前的对象元数据
        ObjectMetadata objectMetadata = cosClient.getObjectMetadata(bucketName, key);
        // 修改对象元数据必须设置 replaced
        objectMetadata.setHeader("x-cos-metadata-directive", "Replaced");

        // 设置新的对象元数据
        // 注意:Content-Disposition 、自定义元数据或者其他有中文的头域值,在设置前请先调用 UrlEncoderUtils.encode(String) 编码,避免签名问题
        objectMetadata.setHeader("x-cos-storage-class", "STANDARD_IA");
        objectMetadata.setContentType("text/plain");

        /**
         * copyObjectRequest:拷贝文件请求
         *
         * 重点:
         * 参数1: 源 Bucket region。默认值:与当前 clientConfig 的 region 一致,表示同地域拷贝
         * 参数23:表示源文件得桶名称和key。
         * 参数45:表示目的桶和key。
         * 如果参数23和参数45相同,则是修改操作。相当于linux命令中,用mv给文件改名。
         */
        Region region = new Region(cosConfig.getProperty("qcloud.region"));
        CopyObjectRequest copyObjectRequest = new CopyObjectRequest(region, bucketName, key, bucketName, key);
        copyObjectRequest.setNewObjectMetadata(objectMetadata);

        try {
            CopyObjectResult copyObjectResult = cosClient.copyObject(copyObjectRequest);
            System.out.println(copyObjectResult.getRequestId());
        } catch (CosServiceException e) {
            e.printStackTrace();
        } catch (CosClientException e) {
            e.printStackTrace();
        }

        cosClient.shutdown();
    }

10、获取对象的访问远程URL

根据key,获得一个完整的可远程访问的该文件的URL。https://abc-1200000000.cos.ap-shanghai.myqcloud.com/test/2.png

其实域名是固定且可知的,在控制台的桶信息里看到,本身就知道完整Key,可以直接本地拼接URL,不需要再一次查询。

    @Test
    public void getURL(){
        String bucketName = cosConfig.getProperty("qcloud.bucketName");
        String key = "/test/2.png";

        URL url = cosClient.getObjectUrl(bucketName, key);
        System.out.println(url);
    }

以下代码只是基本的组件扫描功能。

全是Java的基础知识。

类加载器、反射、注解、文件和文件夹操作、String的处理、集合。

public static void main(String[] args) {
        Map<String, Object> beanMap = new HashMap<>();
        // 1、需要扫描的目录
        String packageName = "cn.qqwer.achong.IOC.bean";
        // 2、将包名转换为路径格式
        String packagePath = packageName.replaceAll("\\.","/"); // cn/qqwer/achong/IOC/bean

        // 3、通过系统加载器获取本地资源路径,返回一个URL对象
        URL url = ClassLoader.getSystemClassLoader().getResource(packagePath);
        // 4、得到绝对路径
        String path = url.getPath();
        // 5、根据绝对路径得到file对象
        File file = new File(path);
        // 6、获取路径下的所有类文件,获得数组
        File[] files = file.listFiles();
        // 7、开始逐个处理
        Arrays.stream(files).forEach(f -> {
            // 8、拼接类文件的全包名
            String className = packageName + "." +f.getName().split("\\.")[0];  // cn.qqwer.achong.IOC.bean.User
            try {
                // 9、根据类的全路径,获取类本身
                Class<?> aClass = Class.forName(className);
                // 10、判断此类是否标记Component注解
                if (aClass.isAnnotationPresent(Component.class)){
                    Component annotation = aClass.getAnnotation(Component.class);
                    // 11、如果有注解、再继续判断@Component是否有值,即是否为bean设置ID
                    String id = annotation.value();
                    if ("".equals(annotation.value())){
                        String simpleName = aClass.getSimpleName();
                        // 12、如果没为bean设置id,则将类文件名设置为bean的ID,类名首字母需转小写
                        if (!Character.isLowerCase(simpleName.charAt(0))){
                            id = new StringBuilder().append(Character.toLowerCase(simpleName.charAt(0))).append(simpleName.substring(1)).toString();
                        }
                    }
                    // 13、最好,将对象new出,并放到一个Map中。
                    Object obj = aClass.newInstance();
                    beanMap.put(id, obj);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        //输出map查看结果
        System.out.println(beanMap);

    }

spring组件扫描原理运行结果.png

正常情况下,js对文本框控件正常的监听是这样子的。

// 对id为 #content 的控件添加文本修改事件
$(document).on("input propertychange","#content",function(res){
    var newValue = $("#content").val();
    console.log(newValue)
});

博客素材2 00_00_00-00_00_30.gif

​ 可以正常监听文本的变化。但是,太过灵敏了,某些情况下可能不适合。

​ 比如这样一个需求:网页有一个实时同步功能的文本框,文本框内的数据需要实时发送到服务器存储,以便下一次展现。

​ 在打字的情况下,每按下一个字母,都会触发一次文本修改。如果修改监听的回调函数里面是向服务器发送请求的话,即使用户在正常的字的情况下,对服务器来说像是遭受CC攻击了一样,频率太快了。

​ 所以,我对代码进行了改进,时间精度换性能。

var n = 0;    
$(document).on("input propertychange", "#content", function(res){    // 设置文本框监听
    var nn = ++n;
    setTimeout(function(){
        if (n != nn) return;
        UpData();
    }, 1000);
});

博客素材3 00_00_00-00_00_30.gif

​ 通过对比文本修改次数来判断需不需要向服务器发送数据。上面代码中定义了两个变量,一个变量n在外面记录 回调函数 文本变化的次数;另一个临时变量nn记录在 回调函数 执行的那一刻文本变化次数。在 回调函数 里用setTimeout来定时2秒执行一个函数,1秒后,如果内外两个记录文本修改次数的变量相同,则向服务器发送数据,否则直接结束本次执行。

​ 如此循环往复,当用户真正停下来,或者用户暂停撰写时,才往服务器发送本次修改的文本。虽然文本不是真正的实时发送,但是慢一两秒对这个需求来说不是很重要。所以说是时间换性能。

记录一个真实情况。
有这样一个需求:前端传进一个实体对象,后端需要将其存储。

此时应该这样写:

controller:

public ReturnInfo regUri(Url url){
    ......
}

mapper:

    <insert id="insertUrl"  parameterType="url">
        INSERT INTO url_info(uri, data, create_time, expiration_time)
                    VALUE (#{uri}, #{data}, #{createTime}, #{expirationTime});
    </insert>

但是,其中有一个属性可能是null,比如,在现在的实体中,expirationTime这个过期时间参数可以为空。这时候可以使用mybatis的动态sql功能的 if标签 对其进行判断,根据expirationTime这个属性的有无来修改sql语句。

    <insert id="insertUrl"  parameterType="url">
        INSERT INTO url_info(uri,data, create_time
                                 <if test="expirationTime != null and expirationTime != ''">
                                     , expiration_time
                                 </if>
                             )
                    VALUE (#{uri},#{data}, #{createTime}
                                <if test="expirationTime != null and expirationTime != ''">
                                    , #{expirationTime}
                                </if>
                            );
    </insert>

使用<if></if>标签可以根据实体属性,判断标签里的内容是否拼接进sql语句中。