標籤: 網頁設計公司

  • js數組方法大全(下)

    js數組方法大全(下)

    # js數組方法大全(下)

    記錄一下整理的js數組方法,免得每次要找方法都找不到。圖片有點多,注意流量,嘻嘻!

    本期分享

    • forEach()
    • map()
    • filer()
    • every()
    • some()
    • reduce()
    • reduceRight()
    • indexOf()
    • lastIndex()

    上期分享

    • join()
    • reverse()
    • sort()
    • concat()
    • slice()
    • splice()
    • push()
    • pop()
    • unshift()
    • shift()
    • toString()
    • toLocaleString()

    forEach() —>遍歷

    • 使用熱度:經常用
    • 是否改變原始數組:否
    • 返回:無
    • 參數:
    參數位置 參數類型 是否必選 說明
    1 function 三個參數分別是:數組元素、元素的索引、數組本身
    • 說明:該方法無法提前終止運行,如果要提前終止運行,只能使用try塊中,然後拋出一個異常。
    • 小技巧:如果數組是個數組對象形式可以直接操作數組元素改變原始數組本身,因為對象是個引用數據類型嘛!
    • 實例如下:
    var log=console.log;
    var data=[1,2,3,4,5];
    var sum =0;
    data.forEach(value=>{
      sum+=value;
    })
    log(sum);
    
    data.forEach((v,i,a)=>{
      a[i]=v+1;
    })
    log(data);
    
    var data_post=[{a:1},{a:2}]
    data_post.forEach(value=>{
      value.a++;
    })
    log(data_post)

    map() —>映射

    • 使用熱度:經常用
    • 是否改變原始數組:否
    • 返回:返回一個新函數
    • 參數:
    參數位置 參數類型 是否必選 說明
    1 function 三個參數分別是:數組元素、元素的索引、數組本身
    • 說明:傳遞給map函數應該有返回值,返回值是新數組的元素。
    • 實例如下:
    var log=console.log;
    var data=[1,2,3,4,5];
    var b= data.map(x=>{
      return x*x;
    })
    log(b)

    filter() —>過濾

    • 使用熱度:常用
    • 是否改變原始數組:否
    • 返回:返回過濾后的數組
    • 參數:
    參數位置 參數類型 是否必選 說明
    1 function 三個參數分別是:數組元素、元素的索引、數組本身
    • 說明:如果返回值是true或者可以轉化為true的值,那麼這個值就是新數組的元素。
    • 實例如下:
    var log=console.log;
    var data=[1,2,3,4,5];
    var b= data.filter(x=>{
      return x<3;
    })
    log(b)

    every() —>檢測

    • 使用熱度:不常用
    • 是否改變原始數組:否
    • 返回:true或者false
    • 參數:
    參數位置 參數類型 是否必選 說明
    1 function 三個參數分別是:數組元素、元素的索引、數組本身
    • 說明:當且僅當針對數組中的所有元素調用綁定函數都返回true時,它才返回true。
    • 注意:一旦every或者some已經確定了改返回什麼值得時候就不會遍曆數組了。根據數學上的慣例,在空數組調用時,every返回true,some返回false。
    • 實例如下:
    var log=console.log;
    var data=[1,2,3,4,5];
    var b= data.every(x=>{
      return x<10;
    })
    log(b)
    
    var c= data.every(x=>{
      return x%2===0;
    })
    log(c)

    some() —>檢測

    • 使用熱度:不常用
    • 是否改變原始數組:否
    • 返回:true或者false
    • 參數:
    參數位置 參數類型 是否必選 說明
    1 function 三個參數分別是:數組元素、元素的索引、數組本身
    • 說明:當數組中至少有一個元素調用綁定函數返回true時,它就返回true。
    • 注意:一旦every或者some已經確定了改返回什麼值得時候就不會遍曆數組了。根據數學上的慣例,在空數組調用時,every返回true,some返回false。
    • 實例如下:
    var log=console.log;
    var data=[1,2,3,4,5];
    var b= data.some(x=>{
      return x>10;
    })
    log(b)
    
    var c= data.some(x=>{
      return x%2===0;
    })
    log(c)

    reduce() —>簡化

    • 使用熱度:不常用
    • 是否改變原始數組:否
    • 返回:返回一個值
    • 參數:
    參數位置 參數類型 是否必選 作用
    1 function 四個參數分別是:初始化值/數組元素、數組元素、元素的索引、數組本身
    2 number 供計算的初始化值
    • 說明:第一個參數是到目前為止的簡化操作累計的結果
    • 注意:如果沒有初始化值,第一次調用函數的第一個參數就是第一個數組元素,第二個參數則是第二個數組元素。如果有初始化值,第一次調用函數的第一個參數就是初始化值,二個參數則是第一個數組元素。
    • 實例如下:
    var log=console.log;
    var data=[1,2,3,4,5];
    var b= data.reduce((x,y)=>{
      return x+y;
    },0)
    log(b)
    
    var c= data.reduce((x,y)=>{
      return x*y;
    },1)
    log(c)
    
    var d= data.reduce((x,y)=>{
      return x>y?x:y;
    },1)
    log(d)

    reduceRight() —>簡化

    • 使用熱度:不常用
    • 是否改變原始數組:否
    • 返回:返回一個值
    • 參數:
    參數位置 參數類型 是否必選 作用
    1 function 四個參數分別是:初始化值/數組元素、數組元素、元素的索引、數組本身
    2 number 供計算的初始化值
    • 說明:第一個參數是到目前為止的簡化操作累計的結果。不同於reduce這個僅僅是從右到左計算。
    • 注意:如果沒有初始化值,第一次調用函數的第一個參數就是第一個數組元素,第二個參數則是第二個數組元素。如果有初始化值,第一次調用函數的第一個參數就是初始化值,二個參數則是第一個數組元素。
    • 實例如下:
    var log=console.log;
    var data=[1,2,3,4,5];
    data.reduceRight((x,y)=>{
      log(y)
      return x+y;
    },0)

    indexOf() —>搜索

    • 使用熱度:經常用
    • 是否改變原始數組:否
    • 返回:返回數組索引或者-1
    • 參數:
    參數位置 參數類型 是否必選 作用
    1 * 要搜索的數組元素
    2 number 從數組哪個索引開始搜索
    • 說明:如果能搜索到結果將返回第一個索引,如果搜索不到就返回-1
    • 注意:如果第二個參數是負數,指的是從數組元素末尾的偏移量位置開始向後搜索,而不是到偏移量位置就截止搜索了,這裡是很容易和splice弄混的。
    • 實例如下:
    var log=console.log;
    var data=[1,2,3,4,5];
    var b=data.indexOf(1,-5);
    log(b);

    lastIndexOf() —>搜索

    • 使用熱度:經常用
    • 是否改變原始數組:否
    • 返回:返回數組索引或者-1
    • 參數:
    參數位置 參數類型 是否必選 作用
    1 * 要搜索的數組元素
    2 number 從數組哪個索引開始搜索
    • 說明:和indexOf不同的是lashIndexOf是反向搜索
    • 注意:因為是反向搜索,當第二個參數是負數的時候,就是指從末尾偏移量的位置向前搜索
    • 實例如下:
    var log=console.log;
    var data=[1,2,3,2,5];
    var b=data.lastIndexOf(2,-3)
    log(b)
    
    var c=data.lastIndexOf(1,-5)
    log(c)

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 6. SOFAJRaft源碼分析— 透過RheaKV看線性一致性讀

    開篇

    其實這篇文章我本來想在講完選舉的時候就開始講線性一致性讀的,但是感覺直接講沒頭沒尾的看起來比比較困難,所以就有了RheaKV的系列,這是RheaKV,終於可以講一下SOFAJRaft的線性一致性讀是怎麼做到了的。所謂線性一致性,一個簡單的例子是在 T1 的時間寫入一個值,那麼在 T1 之後讀一定能讀到這個值,不可能讀到 T1 之前的值。

    其中部分內容參考SOFAJRaft文檔:

    RheaKV讀取數據

    RheaKV的讀取數據的入口是DefaultRheaKVStore的bGet。

    DefaultRheaKVStore#bGet

    public byte[] bGet(final String key) {
        return FutureHelper.get(get(key), this.futureTimeoutMillis);
    }

    bGet方法中會一直調用到DefaultRheaKVStore的一個get方法中:
    DefaultRheaKVStore#get

    private CompletableFuture<byte[]> get(final byte[] key, final boolean readOnlySafe,
                                          final CompletableFuture<byte[]> future, final boolean tryBatching) {
        //校驗started狀態
        checkState();
        Requires.requireNonNull(key, "key");
        if (tryBatching) {
            final GetBatching getBatching = readOnlySafe ? this.getBatchingOnlySafe : this.getBatching;
            if (getBatching != null && getBatching.apply(key, future)) {
                return future;
            }
        }
        internalGet(key, readOnlySafe, future, this.failoverRetries, null, this.onlyLeaderRead);
        return future;
    }

    get方法會根據傳入的參數來判斷是否採用批處理的方式來讀取數據,readOnlySafe表示是否開啟線程一致性讀,由於我們調用的是get方法,所以readOnlySafe和tryBatching都會返回true。
    所以這裡會調用getBatchingOnlySafe的apply方法,將key和future傳入。
    getBatchingOnlySafe是在我們初始化DefaultRheaKVStore的時候初始化的:
    DefaultRheaKVStore#init

    .....
    this.getBatchingOnlySafe = new GetBatching(KeyEvent::new, "get_batching_only_safe",
            new GetBatchingHandler("get_only_safe", true));
    .....

    在初始化getBatchingOnlySafe的時候傳入的處理器是GetBatchingHandler。

    然後我們回到getBatchingOnlySafe#apply中,看看這個方法做了什麼:

    public boolean apply(final byte[] message, final CompletableFuture<byte[]> future) {
        //GetBatchingHandler
        return this.ringBuffer.tryPublishEvent((event, sequence) -> {
            event.reset();
            event.key = message;
            event.future = future;
        });
    }

    apply方法會向Disruptor發送一個事件進行異步處理,並把我們的key封裝到event的key中。getBatchingOnlySafe的處理器是GetBatchingHandler。

    批量獲取數據

    GetBatchingHandler#onEvent

    public void onEvent(final KeyEvent event, final long sequence, final boolean endOfBatch) throws Exception {
        this.events.add(event);
        this.cachedBytes += event.key.length;
        final int size = this.events.size();
        //校驗一下數據量,沒有達到MaxReadBytes並且不是最後一個event,那麼直接返回
        if (!endOfBatch && size < batchingOpts.getBatchSize() && this.cachedBytes < batchingOpts.getMaxReadBytes()) {
            return;
        }
    
        if (size == 1) {
            reset();
            try {
                //如果只是一個get請求,那麼不需要進行批量處理
                get(event.key, this.readOnlySafe, event.future, false);
            } catch (final Throwable t) {
                exceptionally(t, event.future);
            }
        } else {
            //初始化一個剛剛好大小的集合
            final List<byte[]> keys = Lists.newArrayListWithCapacity(size);
            final CompletableFuture<byte[]>[] futures = new CompletableFuture[size];
            for (int i = 0; i < size; i++) {
                final KeyEvent e = this.events.get(i);
                keys.add(e.key);
                futures[i] = e.future;
            }
            //遍歷完events數據到entries之後,重置
            reset();
            try {
                multiGet(keys, this.readOnlySafe).whenComplete((result, throwable) -> {
                    //異步回調處理數據
                    if (throwable == null) {
                        for (int i = 0; i < futures.length; i++) {
                            final ByteArray realKey = ByteArray.wrap(keys.get(i));
                            futures[i].complete(result.get(realKey));
                        }
                        return;
                    }
                    exceptionally(throwable, futures);
                });
            } catch (final Throwable t) {
                exceptionally(t, futures);
            }
        }
    }
    }

    onEvent方法首先會校驗一下當前的event數量有沒有達到閾值以及當前的event是不是Disruptor中最後一個event;然後會根據不同的events集合中的數量來走不同的實現,這裏做了一個優化,如果是只有一條數據那麼不會走批處理;最後將所有的key放入到keys集合中並調用multiGet進行批處理。

    multiGet方法會調用internalMultiGet返回一個Future,從而實現異步的返回結果。
    DefaultRheaKVStore#internalMultiGet

    private FutureGroup<Map<ByteArray, byte[]>> internalMultiGet(final List<byte[]> keys, final boolean readOnlySafe,
                                                                 final int retriesLeft, final Throwable lastCause) {
        //因為不同的key是存放在不同的region中的,所以一個region會對應多個key,封裝到map中
        final Map<Region, List<byte[]>> regionMap = this.pdClient
                .findRegionsByKeys(keys, ApiExceptionHelper.isInvalidEpoch(lastCause));
        //返回值
        final List<CompletableFuture<Map<ByteArray, byte[]>>> futures =
                Lists.newArrayListWithCapacity(regionMap.size());
        //lastCause傳入為null
        final Errors lastError = lastCause == null ? null : Errors.forException(lastCause);
    
        for (final Map.Entry<Region, List<byte[]>> entry : regionMap.entrySet()) {
            final Region region = entry.getKey();
            final List<byte[]> subKeys = entry.getValue();
            //重試次數減1,設置一個重試函數
            final RetryCallable<Map<ByteArray, byte[]>> retryCallable = retryCause -> internalMultiGet(subKeys,
                    readOnlySafe, retriesLeft - 1, retryCause);
            final MapFailoverFuture<ByteArray, byte[]> future = new MapFailoverFuture<>(retriesLeft, retryCallable);
            //發送MultiGetRequest請求,獲取數據
            internalRegionMultiGet(region, subKeys, readOnlySafe, future, retriesLeft, lastError, this.onlyLeaderRead);
            futures.add(future);
        }
        return new FutureGroup<>(futures);
    }

    internalMultiGet里會根據key去組裝region,不同的key會對應不同的region,數據時存在region中的,所以要從不同的region中獲取數據,region和key是一對多的關係所以這裡會封裝成一個map。然後會遍歷regionMap,每個region所對應的數據作為一個批次調用到internalRegionMultiGet方法中,根據不同的情況獲取數據。

    DefaultRheaKVStore#internalRegionMultiGet

    private void internalRegionMultiGet(final Region region, final List<byte[]> subKeys, final boolean readOnlySafe,
                                        final CompletableFuture<Map<ByteArray, byte[]>> future, final int retriesLeft,
                                        final Errors lastCause, final boolean requireLeader) {
        //因為當前的是client,所以這裡會是null
        final RegionEngine regionEngine = getRegionEngine(region.getId(), requireLeader);
        // require leader on retry
        //設置重試函數
        final RetryRunner retryRunner = retryCause -> internalRegionMultiGet(region, subKeys, readOnlySafe, future,
                retriesLeft - 1, retryCause, true);
        final FailoverClosure<Map<ByteArray, byte[]>> closure = new FailoverClosureImpl<>(future,
                false, retriesLeft, retryRunner);
        if (regionEngine != null) {
            if (ensureOnValidEpoch(region, regionEngine, closure)) {
                //如果不是null,那麼會獲取rawKVStore,並從中獲取數據
                final RawKVStore rawKVStore = getRawKVStore(regionEngine);
                if (this.kvDispatcher == null) {
                    rawKVStore.multiGet(subKeys, readOnlySafe, closure);
                } else {
                    //如果是kvDispatcher不為空,那麼放入到kvDispatcher中異步執行
                    this.kvDispatcher.execute(() -> rawKVStore.multiGet(subKeys, readOnlySafe, closure));
                }
            }
        } else {
            final MultiGetRequest request = new MultiGetRequest();
            request.setKeys(subKeys);
            request.setReadOnlySafe(readOnlySafe);
            request.setRegionId(region.getId());
            request.setRegionEpoch(region.getRegionEpoch());
            //調用rpc請求
            this.rheaKVRpcService.callAsyncWithRpc(request, closure, lastCause, requireLeader);
        }
    }

    因為我們這裡是client端調用internalRegionMultiGet方法的,所以是沒有設置regionEngine的,那麼會直接向server的當前region所對應的leader節點發送一個MultiGetRequest請求。

    因為上面的這些方法基本上和put是一致的,我們已經在講過了,所以這裏不重複的講了。

    server端處理MultiGetRequest請求

    MultiGetRequest請求會被KVCommandProcessor所處理,KVCommandProcessor里會根據請求的magic方法返回值來判斷是用什麼方式來進行處理。我們這裡會調用到DefaultRegionKVService的handleMultiGetRequest方法中處理請求。

    public void handleMultiGetRequest(final MultiGetRequest request,
                                      final RequestProcessClosure<BaseRequest, BaseResponse<?>> closure) {
        final MultiGetResponse response = new MultiGetResponse();
        response.setRegionId(getRegionId());
        response.setRegionEpoch(getRegionEpoch());
        try {
            KVParameterRequires.requireSameEpoch(request, getRegionEpoch());
            final List<byte[]> keys = KVParameterRequires.requireNonEmpty(request.getKeys(), "multiGet.keys");
            //調用MetricsRawKVStore的multiGet方法
            this.rawKVStore.multiGet(keys, request.isReadOnlySafe(), new BaseKVStoreClosure() {
    
                @SuppressWarnings("unchecked")
                @Override
                public void run(final Status status) {
                    if (status.isOk()) {
                        response.setValue((Map<ByteArray, byte[]>) getData());
                    } else {
                        setFailure(request, response, status, getError());
                    }
                    closure.sendResponse(response);
                }
            });
        } catch (final Throwable t) {
            LOG.error("Failed to handle: {}, {}.", request, StackTraceUtil.stackTrace(t));
            response.setError(Errors.forException(t));
            closure.sendResponse(response);
        }
    }

    handleMultiGetRequest方法會調用MetricsRawKVStore的multiGet方法來批量獲取數據。

    MetricsRawKVStore#multiGet

    public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
        //實例化MetricsKVClosureAdapter對象
        final KVStoreClosure c = metricsAdapter(closure, MULTI_GET, keys.size(), 0);
        //調用RaftRawKVStore的multiGet方法
        this.rawKVStore.multiGet(keys, readOnlySafe, c);
    }

    multiGet方法會傳入一個MetricsKVClosureAdapter實例,通過這個實例實現異步回調response。然後調用RaftRawKVStore的multiGet方法。

    RaftRawKVStore#multiGet

    public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
        if (!readOnlySafe) {
            this.kvStore.multiGet(keys, false, closure);
            return;
        }
        // KV 存儲實現線性一致讀
        // 調用 readIndex 方法,等待回調執行
        this.node.readIndex(BytesUtil.EMPTY_BYTES, new ReadIndexClosure() {
    
            @Override
            public void run(final Status status, final long index, final byte[] reqCtx) {
                //如果狀態返回成功,
                if (status.isOk()) {
                    RaftRawKVStore.this.kvStore.multiGet(keys, true, closure);
                    return;
                }
                //readIndex 讀取失敗嘗試應用鍵值讀操作申請任務於 Leader 節點的狀態機 KVStoreStateMachine
                RaftRawKVStore.this.readIndexExecutor.execute(() -> {
                    if (isLeader()) {
                        LOG.warn("Fail to [multiGet] with 'ReadIndex': {}, try to applying to the state machine.",
                                status);
                        // If 'read index' read fails, try to applying to the state machine at the leader node
                        applyOperation(KVOperation.createMultiGet(keys), closure);
                    } else {
                        LOG.warn("Fail to [multiGet] with 'ReadIndex': {}.", status);
                        // Client will retry to leader node
                        new KVClosureAdapter(closure, null).run(status);
                    }
                });
            }
        });
    }

    multiGet調用node的readIndex方法進行一致性讀操作,並設置回調,如果返回成功那麼就直接調用RocksRawKVStore讀取數據,如果返回不是成功那麼申請任務於 Leader 節點的狀態機 KVStoreStateMachine。

    線性一致性讀readIndex

    所謂線性一致讀,一個簡單的例子是在 t1 的時刻我們寫入了一個值,那麼在 t1 之後,我們一定能讀到這個值,不可能讀到 t1 之前的舊值(想想 Java 中的 volatile 關鍵字,即線性一致讀就是在分佈式系統中實現 Java volatile 語義)。簡而言之是需要在分佈式環境中實現 Java volatile 語義效果,即當 Client 向集群發起寫操作的請求並且獲得成功響應之後,該寫操作的結果要對所有後來的讀請求可見。和 volatile 的區別在於 volatile 是實現線程之間的可見,而 SOFAJRaft 需要實現 Server 之間的可見。

    SOFAJRaft提供的線性一致讀是基於 Raft 協議的 ReadIndex 實現用 ;Node#readIndex(byte [] requestContext, ReadIndexClosure done) 發起線性一致讀請求,當安全讀取時傳入的 Closure 將被調用,正常情況從狀態機中讀取數據返回給客戶端。

    Node#readIndex

    public void readIndex(final byte[] requestContext, final ReadIndexClosure done) {
        if (this.shutdownLatch != null) {
            //異步執行回調
            Utils.runClosureInThread(done, new Status(RaftError.ENODESHUTDOWN, "Node is shutting down."));
            throw new IllegalStateException("Node is shutting down");
        }
        Requires.requireNonNull(done, "Null closure");
        //EMPTY_BYTES
        this.readOnlyService.addRequest(requestContext, done);
    }

    readIndex會調用ReadOnlyServiceImpl#addRequest將requestContext和回調方法done傳入,requestContext傳入的是BytesUtil.EMPTY_BYTES
    接着往下看

    ReadOnlyServiceImpl#addRequest

    public void addRequest(final byte[] reqCtx, final ReadIndexClosure closure) {
        if (this.shutdownLatch != null) {
            Utils.runClosureInThread(closure, new Status(RaftError.EHOSTDOWN, "Was stopped"));
            throw new IllegalStateException("Service already shutdown.");
        }
        try {
            EventTranslator<ReadIndexEvent> translator = (event, sequence) -> {
                event.done = closure;
                //EMPTY_BYTES
                event.requestContext = new Bytes(reqCtx);
                event.startTime = Utils.monotonicMs();
            };
            int retryTimes = 0;
            while (true) {
                //ReadIndexEventHandler
                if (this.readIndexQueue.tryPublishEvent(translator)) {
                    break;
                } else {
                    retryTimes++;
                    if (retryTimes > MAX_ADD_REQUEST_RETRY_TIMES) {
                        Utils.runClosureInThread(closure,
                            new Status(RaftError.EBUSY, "Node is busy, has too many read-only requests."));
                        this.nodeMetrics.recordTimes("read-index-overload-times", 1);
                        LOG.warn("Node {} ReadOnlyServiceImpl readIndexQueue is overload.", this.node.getNodeId());
                        return;
                    }
                    ThreadHelper.onSpinWait();
                }
            }
        } catch (final Exception e) {
            Utils.runClosureInThread(closure, new Status(RaftError.EPERM, "Node is down."));
        }
    }

    addRequest方法里會將傳入的reqCtx和closure封裝成一個時間,傳入到readIndexQueue隊列中,事件發布成功後會交由ReadIndexEventHandler處理器處理,發布失敗會進行重試,最多重試3次。

    ReadIndexEventHandler

    private class ReadIndexEventHandler implements EventHandler<ReadIndexEvent> {
        // task list for batch
        private final List<ReadIndexEvent> events = new ArrayList<>(
                                                      ReadOnlyServiceImpl.this.raftOptions.getApplyBatch());
    
        @Override
        public void onEvent(final ReadIndexEvent newEvent, final long sequence, final boolean endOfBatch)
                                                                                                         throws Exception {
            if (newEvent.shutdownLatch != null) {
                executeReadIndexEvents(this.events);
                this.events.clear();
                newEvent.shutdownLatch.countDown();
                return;
            }
    
            this.events.add(newEvent);
            //批量執行
            if (this.events.size() >= ReadOnlyServiceImpl.this.raftOptions.getApplyBatch() || endOfBatch) {
                executeReadIndexEvents(this.events);
                this.events.clear();
            }
        }
    }

    ReadIndexEventHandler是ReadOnlyServiceImpl裏面的內部類,裏面有一個全局的events集合用來做事件的批處理,如果當前的event已經達到了32個或是整個Disruptor隊列里最後一個那麼會調用ReadOnlyServiceImpl的executeReadIndexEvents方法進行事件的批處理。

    ReadOnlyServiceImpl#executeReadIndexEvents

    private void executeReadIndexEvents(final List<ReadIndexEvent> events) {
        if (events.isEmpty()) {
            return;
        }
        //初始化ReadIndexRequest
        final ReadIndexRequest.Builder rb = ReadIndexRequest.newBuilder() //
            .setGroupId(this.node.getGroupId()) //
            .setServerId(this.node.getServerId().toString());
    
        final List<ReadIndexState> states = new ArrayList<>(events.size());
    
        for (final ReadIndexEvent event : events) {
            rb.addEntries(ZeroByteStringHelper.wrap(event.requestContext.get()));
            states.add(new ReadIndexState(event.requestContext, event.done, event.startTime));
        }
        final ReadIndexRequest request = rb.build();
    
        this.node.handleReadIndexRequest(request, new ReadIndexResponseClosure(states, request));
    }

    executeReadIndexEvents封裝好ReadIndexRequest請求和將ReadIndexState集合封裝到ReadIndexResponseClosure中,為後續的操作做裝備

    NodeImpl#handleReadIndexRequest

    public void handleReadIndexRequest(final ReadIndexRequest request, final RpcResponseClosure<ReadIndexResponse> done) {
        final long startMs = Utils.monotonicMs();
        this.readLock.lock();
        try {
            switch (this.state) {
                case STATE_LEADER:
                    readLeader(request, ReadIndexResponse.newBuilder(), done);
                    break;
                case STATE_FOLLOWER:
                    readFollower(request, done);
                    break;
                case STATE_TRANSFERRING:
                    done.run(new Status(RaftError.EBUSY, "Is transferring leadership."));
                    break;
                default:
                    done.run(new Status(RaftError.EPERM, "Invalid state for readIndex: %s.", this.state));
                    break;
            }
        } finally {
            this.readLock.unlock();
            this.metrics.recordLatency("handle-read-index", Utils.monotonicMs() - startMs);
            this.metrics.recordSize("handle-read-index-entries", request.getEntriesCount());
        }
    }

    因為線性一致讀在任何集群內的節點發起,並不需要強制要求放到 Leader 節點上,允許在 Follower 節點執行,因此大大降低 Leader 的讀取壓力。
    當在Follower節點執行一致性讀的時候實際上Follower 節點調用 RpcService#readIndex(leaderId.getEndpoint(), newRequest, -1, closure) 方法向 Leader 發送 ReadIndex 請求,交由Leader節點實現一致性讀。所以我這裏主要介紹Leader的一致性讀。

    繼續往下走調用NodeImpl的readLeader方法
    NodeImpl#readLeader

    private void readLeader(final ReadIndexRequest request, final ReadIndexResponse.Builder respBuilder,
                            final RpcResponseClosure<ReadIndexResponse> closure) {
        //1. 獲取集群節點中多數選票數是多少
        final int quorum = getQuorum();
        if (quorum <= 1) {
            // Only one peer, fast path.
            //如果集群中只有一個節點,那麼直接調用回調函數,返回成功
            respBuilder.setSuccess(true) //
                    .setIndex(this.ballotBox.getLastCommittedIndex());
            closure.setResponse(respBuilder.build());
            closure.run(Status.OK());
            return;
        }
    
        final long lastCommittedIndex = this.ballotBox.getLastCommittedIndex();
        //2. 任期必須相等
        //日誌管理器 LogManager 基於投票箱 BallotBox 的 lastCommittedIndex 獲取任期檢查是否等於當前任期
        // 如果不等於當前任期表示此 Leader 節點未在其任期內提交任何日誌,需要拒絕只讀請求;
        if (this.logManager.getTerm(lastCommittedIndex) != this.currTerm) {
            // Reject read only request when this leader has not committed any log entry at its term
            closure
                    .run(new Status(
                            RaftError.EAGAIN,
                            "ReadIndex request rejected because leader has not committed any log entry at its term, " +
                             "logIndex=%d, currTerm=%d.",
                            lastCommittedIndex, this.currTerm));
            return;
        }
        respBuilder.setIndex(lastCommittedIndex);
    
        if (request.getPeerId() != null) {
            // request from follower, check if the follower is in current conf.
            final PeerId peer = new PeerId();
            peer.parse(request.getServerId());
            //3. 來自 Follower 的請求需要檢查 Follower 是否在當前配置
            if (!this.conf.contains(peer)) {
                closure
                        .run(new Status(RaftError.EPERM, "Peer %s is not in current configuration: {}.", peer,
                         this.conf));
                return;
            }
        }
    
        ReadOnlyOption readOnlyOpt = this.raftOptions.getReadOnlyOptions();
        //4. 如果使用的是ReadOnlyLeaseBased,確認leader是否是在在租約有效時間內
        if (readOnlyOpt == ReadOnlyOption.ReadOnlyLeaseBased && !isLeaderLeaseValid()) {
            // If leader lease timeout, we must change option to ReadOnlySafe
            readOnlyOpt = ReadOnlyOption.ReadOnlySafe;
        }
    
        switch (readOnlyOpt) {
            //5
            case ReadOnlySafe:
                final List<PeerId> peers = this.conf.getConf().getPeers();
                Requires.requireTrue(peers != null && !peers.isEmpty(), "Empty peers");
                //設置心跳的響應回調函數
                final ReadIndexHeartbeatResponseClosure heartbeatDone = new ReadIndexHeartbeatResponseClosure(closure,
                        respBuilder, quorum, peers.size());
                // Send heartbeat requests to followers
                //向 Followers 節點發起一輪 Heartbeat,如果半數以上節點返回對應的
                // Heartbeat Response,那麼 Leader就能夠確定現在自己仍然是 Leader
                for (final PeerId peer : peers) {
                    if (peer.equals(this.serverId)) {
                        continue;
                    }
                    this.replicatorGroup.sendHeartbeat(peer, heartbeatDone);
                }
                break;
            //6. 因為在租約期內不會發生選舉,確保 Leader 不會變化
            //所以直接返回回調結果
            case ReadOnlyLeaseBased:
                // Responses to followers and local node.
                respBuilder.setSuccess(true);
                closure.setResponse(respBuilder.build());
                closure.run(Status.OK());
                break;
        }
    }
    1. 獲取集群節點中多數選票數是多少,即集群節點的1/2+1,如果當前的集群里只有一個節點,那麼直接返回成功,並調用回調方法
    2. 校驗 Raft 集群節點數量以及 lastCommittedIndex 所屬任期符合預期,那麼響應構造器設置其索引為投票箱 BallotBox 的 lastCommittedIndex
    3. 來自 Follower 的請求需要檢查 Follower 是否在當前配置,如果不在當前配置中直接調用回調方法設置異常
    4. 獲取 ReadIndex 請求級別 ReadOnlyOption 配置,ReadOnlyOption 參數默認值為 ReadOnlySafe。如果設置的是ReadOnlyLeaseBased,那麼會調用isLeaderLeaseValid檢查leader是否是在在租約有效時間內
    5. 配置為ReadOnlySafe 調用 Replicator#sendHeartbeat(rid, closure) 方法向 Followers 節點發送 Heartbeat 心跳請求,發送心跳成功執行 ReadIndexHeartbeatResponseClosure 心跳響應回調;ReadIndex 心跳響應回調檢查是否超過半數節點包括 Leader 節點自身投票贊成,半數以上節點返回客戶端Heartbeat 請求成功響應,即 applyIndex 超過 ReadIndex 說明已經同步到 ReadIndex 對應的 Log 能夠提供 Linearizable Read
    6. 配置為ReadOnlyLeaseBased,因為Leader 租約有效期間認為當前 Leader 是 Raft Group 內的唯一有效 Leader,所以忽略 ReadIndex 發送 Heartbeat 確認身份步驟,直接返回 Follower 節點和本地節點 Read 請求成功響應。Leader 節點繼續等待狀態機執行,直到 applyIndex 超過 ReadIndex 安全提供 Linearizable Read

    無論是ReadOnlySafe還是ReadOnlyLeaseBased,最後發送成功響應都會調用ReadIndexResponseClosure的run方法。

    ReadIndexResponseClosure#run

    public void run(final Status status) {
        //fail
        //傳入的狀態不是ok,響應失敗
        if (!status.isOk()) {
            notifyFail(status);
            return;
        }
        final ReadIndexResponse readIndexResponse = getResponse();
        //Fail
        //response沒有響應成功,響應失敗
        if (!readIndexResponse.getSuccess()) {
            notifyFail(new Status(-1, "Fail to run ReadIndex task, maybe the leader stepped down."));
            return;
        }
        // Success
        //一致性讀成功
        final ReadIndexStatus readIndexStatus = new ReadIndexStatus(this.states, this.request,
            readIndexResponse.getIndex());
        for (final ReadIndexState state : this.states) {
            // Records current commit log index.
            //設置當前提交的index
            state.setIndex(readIndexResponse.getIndex());
        }
    
        boolean doUnlock = true;
        ReadOnlyServiceImpl.this.lock.lock();
        try {
            //校驗applyIndex 是否超過 ReadIndex
            if (readIndexStatus.isApplied(ReadOnlyServiceImpl.this.fsmCaller.getLastAppliedIndex())) {
                // Already applied, notify readIndex request.
                ReadOnlyServiceImpl.this.lock.unlock();
                doUnlock = false;
                //已經同步到 ReadIndex 對應的 Log 能夠提供 Linearizable Read
                notifySuccess(readIndexStatus);
            } else {
                // Not applied, add it to pending-notify cache.
                ReadOnlyServiceImpl.this.pendingNotifyStatus
                    .computeIfAbsent(readIndexStatus.getIndex(), k -> new ArrayList<>(10)) //
                    .add(readIndexStatus);
            }
        } finally {
            if (doUnlock) {
                ReadOnlyServiceImpl.this.lock.unlock();
            }
        }
    }

    Run方法首先會校驗一下是否需要響應失敗,如果響應成功,那麼會將所有封裝的ReadIndexState更新一下index,然後校驗一下applyIndex 是否超過 ReadIndex,超過了ReadIndex代表所有已經複製到多數派上的 Log(可視為寫操作)被視為安全的 Log,該 Log 所體現的數據就能對客戶端 Client 可見。

    ReadOnlyServiceImpl#notifySuccess

    private void notifySuccess(final ReadIndexStatus status) {
        final long nowMs = Utils.monotonicMs();
        final List<ReadIndexState> states = status.getStates();
        final int taskCount = states.size();
        for (int i = 0; i < taskCount; i++) {
            final ReadIndexState task = states.get(i);
            final ReadIndexClosure done = task.getDone(); // stack copy
            if (done != null) {
                this.nodeMetrics.recordLatency("read-index", nowMs - task.getStartTimeMs());
                done.setResult(task.getIndex(), task.getRequestContext().get());
                done.run(Status.OK());
            }
        }
    }

    如果是響應成功,那麼會調用notifySuccess方法,會將status里封裝的ReadIndexState集合遍歷一遍,調用當中的run方法。

    這個run方法會調用到我們在multiGet中設置的run方法中
    RaftRawKVStore#multiGet

    public void multiGet(final List<byte[]> keys, final boolean readOnlySafe, final KVStoreClosure closure) {
        if (!readOnlySafe) {
            this.kvStore.multiGet(keys, false, closure);
            return;
        }
        // KV 存儲實現線性一致讀
        // 調用 readIndex 方法,等待回調執行
        this.node.readIndex(BytesUtil.EMPTY_BYTES, new ReadIndexClosure() {
    
            @Override
            public void run(final Status status, final long index, final byte[] reqCtx) {
                //如果狀態返回成功,
                if (status.isOk()) {
                    RaftRawKVStore.this.kvStore.multiGet(keys, true, closure);
                    return;
                }
                //readIndex 讀取失敗嘗試應用鍵值讀操作申請任務於 Leader 節點的狀態機 KVStoreStateMachine
                RaftRawKVStore.this.readIndexExecutor.execute(() -> {
                    if (isLeader()) {
                        LOG.warn("Fail to [multiGet] with 'ReadIndex': {}, try to applying to the state machine.",
                                status);
                        // If 'read index' read fails, try to applying to the state machine at the leader node
                        applyOperation(KVOperation.createMultiGet(keys), closure);
                    } else {
                        LOG.warn("Fail to [multiGet] with 'ReadIndex': {}.", status);
                        // Client will retry to leader node
                        new KVClosureAdapter(closure, null).run(status);
                    }
                });
            }
        });

    這個run方法會調用RaftRawKVStore的multiGet從RocksDB中直接獲取數據。

    總結

    我們這篇文章從RheaKVStore的客戶端get方法一直講到,RheaKVStore服務端使用JRaft實現線性一致性讀,並講解了線性一致性讀是怎麼實現的,通過這個例子大家應該對線性一致性讀有了一個相對不錯的理解了。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 讀《MySQL必知必會》我學到了什麼?

    讀《MySQL必知必會》我學到了什麼?

    前言

    最近在寫項目的時候發現自己的SQL基本功有些薄弱,遂上知乎查詢MYSQL關鍵字,期望得到某些高贊答案的指點,於是乎發現了

    https://www.zhihu.com/question/34840297/answer/272185020 這位老兄的建議的書單,根據他的建議首先拜讀了《MYSQL必知必會》這本書,整體講的很基礎,頁數也不多一共 253 頁,適合基礎比較薄弱的同學進行食用。然後循序漸進,閱讀更深層次的書籍進行自我提升。這裏記載了自己在閱讀的過程中記錄的一些關鍵內容,分享給大家。書本 PDF 可以在上面的知乎鏈接獲取,或者點擊 http://www.notedeep.com/note/38/page/282 前往老哥的深度筆記進行下載。

    閱讀心得

    SQL語句和大小寫

    SQL語句不區分大小寫,並且在 Windows 環境下,4.1.1版本之後(現在常用的都是 5.6/5.7/8.0+),MYSQL表名,字段名也是不區分大小寫的,因此我們在命名的時候建議使用單個單詞_單個單詞的形式命名,如:mysql_crash_course user_role

    這裏附上阿里代碼規範的一條強制要求:

    【強制】表名、字段名必須使用小寫字母或数字,禁止出現数字開頭,禁止兩個下劃線中間只出現数字。數據庫字段名的修改代價很大,因為無法進行預發布,所以字段名稱需要慎重考慮。
    說明: MySQL 在 Windows 下不區分大小寫,但在 Linux 下默認是區分大小寫。因此,數據庫名、表名、字段名,都不允許出現任何大寫字母,避免節外生枝。

    不能部分使用DISTINCT

    • DISTINCT 關鍵字應用於所有列而不僅是前置它的列。如果給出 SELECT DISTINCT vend_id,prod_price ,除非指定的兩個列都不同,否則所有行都將被檢索出來。
    • DISTINCT 不會返回其後面跟的所有字段都相同的列。即兩行中SELECT查詢的任何一個字段都相同才會被去重。不能通過DISTINCT加括號 () 等方式來對單個字段進行去重。

    比如項目中有這麼一個需求:

    需要分頁查詢綁了某收費標準的房屋,因為是房屋列表查詢,我們默認相同ID的房屋只出現一次,錯誤的SQL如下:

    房屋表

    收費標準表

    SELECT DISTINCT h.id, h.num, s.name 
    FROM house h 
    LEFT JOIN standard s 
    ON h.id = s.room_id 
    WHERE s.name = '物業費' OR s.name = '暖氣費';

    這條語句並不能返回想要的結果,即每套房屋只出現一次,因為不同的收費標準名稱不一樣,DISTINCT 不能對部分查詢條件去重。可以看到房號為1-001的記錄出現了兩次。

    不過其實按照需求的描述我這裏僅查詢房屋的信息,對於查詢結果來說同一條記錄的房屋信息肯定完全相同,因此 DISTINCT 在我的業務中滿足要求。而有其他業務需要此關鍵字的時候,請大家慎重使用,切記不能部分使用該關鍵字

    區分大小寫和排序順序

    在對文本性的數據進行排序時,A與a相同嗎?a位於B之前還是位於Z之後?

    在創建字段時可以指定字符集,一般使用 utf8mb4, 此時可以選擇相應的排序規則。

    1. utf8mb4_general_ci ci即大小寫不敏感,排序時忽略大小寫,A a 視作相同
    2. utf8mb4_bin / 帶 cs 的即大小寫敏感,相應的升序排列的話, A~Z 在前,小a~z在後
      相應的,在設置大小寫敏感后,查詢條件 where cs = ‘a’ 只能查找到表中該字段為小寫 a 的行。而不敏感,即ci時,A,a都可以被查詢出來。

    BETWEEN關鍵字的注意事項

    在區間查詢時,我們最關注的不應該是區間內的能否被匹配到,因為這是肯定的。而區間的邊界能否被匹配才是我們應該注意的知識點,BETWEEN AND 關鍵字匹配區間時,包含左右邊界條件。如下面的 SQL 執行結果如下:

    SELECT prod_name, prod_price 
    FROM products p
    WHERE p.`prod_price` BETWEEN 5.99 AND 10

    NULL 與不匹配

    在通過過濾選擇出不具有特定值的行時,你可能希望返回具有 NULL 值的行。但是,不行。常見的錯誤會發生在is_xxx 字段上,我經常有這個毛病,

    對於 is_delete 字段,我認為為 null 或者 0 都是未刪除的房屋,所以當我使用

    SELECT * FROM house WHERE is_delete = '0';

    查詢未刪除的房屋時,我只能查到 id 為 3 的房屋,這顯然與我的預期是不符的,解決辦法是where後面加 or is_delete is null ,或者給 is_delete 列默認值 0;建議使用後者。

    這裏附上阿里代碼手冊的一條強制項目:

    【強制】表達是與否概念的字段,必須使用 is_xxx 的方式命名,數據類型是 unsigned tinyint(1 表示是,0 表示否)。說明:任何字段如果為非負數,必須是 unsigned 。

    解釋:tinyint 相當於 Java 中的 byte,取值範圍 -128 ~ 127 ,用來表達是否長度已經足夠,也可以用來表示人的年齡。而 unsigned 表示無符號的,對於確定為非負數的字段,使用 unsigned 可以將取值範圍擴大一倍。

    AND 和 OR 的計算次序

    舉個例子:假如需要列出價格為10美元(含)以上且由 1002 或 1003 製造的所有產品。下面的 SELECT 語句使用 AND 和 OR 操作符的組合建立了一個WHERE 子句:

    SELECT *
    FROM products
    WHERE vend_id = 1002 OR vend_id = 1003 AND prod_price >= 10;

    查詢結果如下:

    而被我用紅框標註的行很顯然不是我們需要的行,為什麼會這樣呢?原因在於計算的次序。SQL(像多數語言一樣)在處理 OR 操作符前,優先處理 AND 操作符。當SQL看到上述 WHERE 子句時,它理解為由供應商 1003 製造的任何價格為10美元(含)以上的產品,或者由供應商 1002 製造的任何產品,而不管其價格如何。換句話說,由於 AND 在計算次序中優先級更高,操作符被錯誤地組合了。解決方法就是加 ();正確的 SQL 如下:

    SELECT *
    FROM products
    WHERE (vend_id = 1002 OR vend_id = 1003) AND prod_price >= 10;

    建議在任何時候使用具有 AND 和 OR 操作符的 WHERE 子句,都應該使用圓括號明確地分組操作符。不要過分依賴默認計算次序,即使它確實是你想要的東西也是如此。使用圓括號沒有什麼壞處,它能消除歧義。

    UNION 組合查詢

    • 利用 UNION ,可給出多條SELECT 語句,將它們的結果組合成單個結果集。
    • UNION 中的每個查詢必須包含相同的列、表達式或聚集函數。
    • 在使用UNION 時,重複的行被自動取消。這是 UNION 的默認行為,但是如果需要,可以改變它。事實上,如果想返回所有匹配行,可使用 UNION ALL 而不是 UNION 。
    • 在用 UNION 組合查詢時,只能使用一條 ORDER BY 子句,它必須出現在最後一條 SELECT 語句之後

    INSERT語句總是使用列的列表

    一般不要使用沒有明確給出列的列表的 INSERT 語句。使用列的列表能使SQL代碼繼續發揮作用,即使表結構發生了變化。實際開發中有可能由於業務的需要,對錶結構進行修改,添加/刪除某一列。這時如果代碼中使用的SQL語句是沒有明確列表的插入語句就會報錯。當然一般我們使用逆向工程生成的 insertSelective(POJO) 並不存在這個問題,因為它對應生成的 SQL 會為我們生成列的列表。

    小心使用更新和刪除語句

    MySQL 沒有撤銷按鈕,因此在使用 UPDATE / DELETE 時一定要加上 WHERE 條件,並且在執行更新/刪除操作之前先進行 SELECT 操作,開啟事務。在執行結束后核對影響的行數和 SELECT 查詢出來的行數一致后再 COMMIT;

    另外,使用 ALTER TABLE 要極為小心,應該在進行改動前做一個完整的備份(模式和數據的備份)。數據庫表的更改不能撤銷,如果增加了不需要的列,可能不能刪除它們。類似地,如果刪除了不應該刪除的列,可能會丟失該列中的所有數據。

    視圖的規則和限制

    • 與表一樣,視圖必須唯一命名(不能給視圖取與別的視圖或表相同的名字)。
    • 對於可以創建的視圖數目沒有限制。
    • 為了創建視圖,必須具有足夠的訪問權限。這些限制通常由數據庫管理人員授予。
    • 視圖可以嵌套,即可以利用從其他視圖中檢索數據的查詢來構造一個視圖。
    • ORDER BY 可以用在視圖中,但如果從該視圖檢索數據 SELECT 中也含有 ORDER BY ,那麼該視圖中的 ORDER BY 將被覆蓋。
    • 視圖不能索引,也不能有關聯的觸發器或默認值。
    • 視圖可以和表一起使用。例如,編寫一條聯結表和視圖的 SELECT語句。

    其中視圖不能索引這一點要格外注意,在我開發過程中遇到過這樣一個視圖:使用 UNION 連結了好幾張表進行查詢,對查詢的結果使用 WHERE 條件再過濾,這裏雖然對於被連接的表對於 WHERE 條件后的字段都建立了索引,但是使用 UNION 連結生成視圖的臨時表並不能擁有索引。因此查詢的效率會很慢!所以建議在 UNION 查詢中將 WHERE 條件放在每個 SELECT 語句中。

    改善性能的一些建議

    • 首先,MySQL(與所有DBMS一樣)具有特定的硬件建議。在學習和研究MySQL時,使用任何舊的計算機作為服務器都可以。但
      對用於生產的服務器來說,應該堅持遵循這些硬件建議。

    • 一般來說,關鍵的生產DBMS應該運行在自己的專用服務器上。

    • MySQL是用一系列的默認設置預先配置的,從這些設置開始通常是很好的。但過一段時間后你可能需要調整內存分配、緩衝區大小等。(為查看當前設置,可使用 SHOW VARIABLES; 和 SHOWSTATUS; )

    • MySQL一個多用戶多線程的DBMS,換言之,它經常同時執行多個任務。如果這些任務中的某一個執行緩慢,則所有請求都會執
      行緩慢。如果你遇到顯著的性能不良,可使用 SHOW PROCESSLIST显示所有活動進程(以及它們的線程ID和執行時間)。你還可以用KILL 命令終結某個特定的進程(使用這個命令需要作為管理員登錄)。

    • 總是有不止一種方法編寫同一條 SELECT 語句。應該試驗聯結、並、子查詢等,找出最佳的方法。

    • 使用 EXPLAIN 語句讓MySQL解釋它將如何執行一條 SELECT 語句。

    • 一般來說,存儲過程執行得比一條一條地執行其中的各條MySQL語句快。但存儲過程一般難以調試和擴展,並且沒有移植性,因此阿里代碼規約裏面強制禁止使用存儲過程

    • 應該總是使用正確的數據類型。

    • 決不要檢索比需求還要多的數據。換言之,不要用 SELECT * (除非你真正需要每個列)。

    • 有的操作(包括 INSERT )支持一個可選的 DELAYED 關鍵字,如果使用它,將把控制立即返回給調用程序,並且一旦有可能就實際執行該操作。

      ​ 延遲插入,當插入和查詢併發執行時,插入被放入等待隊列中。直至所有查詢執行完畢后執行插入。並且MYSQL會在收到插入請求后直接返回給客戶端狀態信息,既是INSERT語句還在隊列中

    • 在導入數據時,應該關閉自動提交。你可能還想刪除索引(包括FULLTEXT 索引),然後在導入完成后再重建它們。

    • 必須索引數據庫表以改善數據檢索的性能。確定索引什麼不是一件微不足道的任務,需要分析使用的 SELECT 語句以找出重複的WHERE 和 ORDER BY 子句。如果一個簡單的 WHERE 子句返回結果所花的時間太長,則可以斷定其中使用的列(或幾個列)就是需要索引的對象。

    • 你的 SELECT 語句中有一系列複雜的 OR 條件嗎?通過使用多條SELECT 語句和連接它們的 UNION 語句,你能看到極大的性能改進。

    • 索引改善數據檢索的性能,但損害數據插入、刪除和更新的性能。如果你有一些表,它們收集數據且不經常被搜索,則在有必要之前不要索引它們。(索引可根據需要添加和刪除。)

    • LIKE 很慢。一般來說,最好是使用 FULLTEXT 而不是 LIKE 。但是 MYSQL FULLTEXT 對漢字並不友好,如果需要使用全文索引,建議使用搜索引擎 ES,可以參考我的另一篇博客: https://www.cnblogs.com/keatsCoder/p/11341835.html

    • 數據庫是不斷變化的實體。一組優化良好的表一會兒后可能就面目全非了。由於表的使用和內容的更改,理想的優化和配置也會改變。

    • 最重要的規則就是,每條規則在某些條件下都會被打破。

    實際開發過程中,我們需要根據業務的需要,開啟慢查詢日誌,然後針對慢SQL,不斷地進行 EXPLAIN 與修改SQL和索引,以求達到 ref 級別,至少達到 range 級別。這就需要強大的內功支持而不是每次都通過百度來解決,希望閱讀此篇文章的你和我一起不斷修鍊。加油!

    常用的函數

    文本處理函數

    日期和時間處理函數

    數值處理函數

    聚合函數

    • 雖然 MAX() 一般用來找出最大的數值或日期值,但MySQL允許將它用來返回任意列中的最大值,包括返迴文本列中的最大值。在用於文本數據時,如果數據按相應的列排序,則 MAX() 返回最後一行。MIN() 函數類似
    • MAX() 函數忽略列值為 NULL 的行。MIN() 函數類似,SUM() 也是

    附錄

    由於數中所附的附件內容(建表語句即數據插入語句)需要外網才能訪問,為了方便大家使用。這裏我已經下載下來附在了這篇博客裏面。如果不需要這些語句,可以直接通過網頁右邊的目錄跳躍到下一章進行瀏覽

    建表語句

    ########################################
    # MySQL Crash Course MYSQL必知必會建表語句
    # http://www.forta.com/books/0672327120/
    # 提供者博客園:后青春期的Keats 複製請註明出處
    ########################################
    
    
    ########################
    # Create customers table
    ########################
    CREATE TABLE customers
    (
      cust_id      int       NOT NULL AUTO_INCREMENT,
      cust_name    char(50)  NOT NULL ,
      cust_address char(50)  NULL ,
      cust_city    char(50)  NULL ,
      cust_state   char(5)   NULL ,
      cust_zip     char(10)  NULL ,
      cust_country char(50)  NULL ,
      cust_contact char(50)  NULL ,
      cust_email   char(255) NULL ,
      PRIMARY KEY (cust_id)
    ) ENGINE=InnoDB;
    
    #########################
    # Create orderitems table
    #########################
    CREATE TABLE orderitems
    (
      order_num  int          NOT NULL ,
      order_item int          NOT NULL ,
      prod_id    char(10)     NOT NULL ,
      quantity   int          NOT NULL ,
      item_price decimal(8,2) NOT NULL ,
      PRIMARY KEY (order_num, order_item)
    ) ENGINE=InnoDB;
    
    
    #####################
    # Create orders table
    #####################
    CREATE TABLE orders
    (
      order_num  int      NOT NULL AUTO_INCREMENT,
      order_date datetime NOT NULL ,
      cust_id    int      NOT NULL ,
      PRIMARY KEY (order_num)
    ) ENGINE=InnoDB;
    
    #######################
    # Create products table
    #######################
    CREATE TABLE products
    (
      prod_id    char(10)      NOT NULL,
      vend_id    int           NOT NULL ,
      prod_name  char(255)     NOT NULL ,
      prod_price decimal(8,2)  NOT NULL ,
      prod_desc  text          NULL ,
      PRIMARY KEY(prod_id)
    ) ENGINE=InnoDB;
    
    ######################
    # Create vendors table
    ######################
    CREATE TABLE vendors
    (
      vend_id      int      NOT NULL AUTO_INCREMENT,
      vend_name    char(50) NOT NULL ,
      vend_address char(50) NULL ,
      vend_city    char(50) NULL ,
      vend_state   char(5)  NULL ,
      vend_zip     char(10) NULL ,
      vend_country char(50) NULL ,
      PRIMARY KEY (vend_id)
    ) ENGINE=InnoDB;
    
    ###########################
    # Create productnotes table
    ###########################
    CREATE TABLE productnotes
    (
      note_id    int           NOT NULL AUTO_INCREMENT,
      prod_id    char(10)      NOT NULL,
      note_date datetime       NOT NULL,
      note_text  text          NULL ,
      PRIMARY KEY(note_id),
      FULLTEXT(note_text)
    ) ENGINE=MyISAM;
    
    
    #####################
    # Define foreign keys
    #####################
    ALTER TABLE orderitems ADD CONSTRAINT fk_orderitems_orders FOREIGN KEY (order_num) REFERENCES orders (order_num);
    ALTER TABLE orderitems ADD CONSTRAINT fk_orderitems_products FOREIGN KEY (prod_id) REFERENCES products (prod_id);
    ALTER TABLE orders ADD CONSTRAINT fk_orders_customers FOREIGN KEY (cust_id) REFERENCES customers (cust_id);
    ALTER TABLE products ADD CONSTRAINT fk_products_vendors FOREIGN KEY (vend_id) REFERENCES vendors (vend_id);

    數據語句

    ########################################
    # MySQL Crash Course MYSQL必知必會數據語句
    # http://www.forta.com/books/0672327120/
    # 提供者博客園:后青春期的Keats 複製請註明出處
    ########################################
    
    
    ##########################
    # Populate customers table
    ##########################
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
    VALUES(10001, 'Coyote Inc.', '200 Maple Lane', 'Detroit', 'MI', '44444', 'USA', 'Y Lee', 'ylee@coyote.com');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact)
    VALUES(10002, 'Mouse House', '333 Fromage Lane', 'Columbus', 'OH', '43333', 'USA', 'Jerry Mouse');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
    VALUES(10003, 'Wascals', '1 Sunny Place', 'Muncie', 'IN', '42222', 'USA', 'Jim Jones', 'rabbit@wascally.com');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
    VALUES(10004, 'Yosemite Place', '829 Riverside Drive', 'Phoenix', 'AZ', '88888', 'USA', 'Y Sam', 'sam@yosemite.com');
    INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact)
    VALUES(10005, 'E Fudd', '4545 53rd Street', 'Chicago', 'IL', '54545', 'USA', 'E Fudd');
    
    
    ########################
    # Populate vendors table
    ########################
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1001,'Anvils R Us','123 Main Street','Southfield','MI','48075', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1002,'LT Supplies','500 Park Street','Anytown','OH','44333', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1003,'ACME','555 High Street','Los Angeles','CA','90046', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1004,'Furball Inc.','1000 5th Avenue','New York','NY','11111', 'USA');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1005,'Jet Set','42 Galaxy Road','London', NULL,'N16 6PS', 'England');
    INSERT INTO vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)
    VALUES(1006,'Jouets Et Ours','1 Rue Amusement','Paris', NULL,'45678', 'France');
    
    
    #########################
    # Populate products table
    #########################
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('ANV01', 1001, '.5 ton anvil', 5.99, '.5 ton anvil, black, complete with handy hook');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('ANV02', 1001, '1 ton anvil', 9.99, '1 ton anvil, black, complete with handy hook and carrying case');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('ANV03', 1001, '2 ton anvil', 14.99, '2 ton anvil, black, complete with handy hook and carrying case');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('OL1', 1002, 'Oil can', 8.99, 'Oil can, red');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('FU1', 1002, 'Fuses', 3.42, '1 dozen, extra long');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('SLING', 1003, 'Sling', 4.49, 'Sling, one size fits all');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('TNT1', 1003, 'TNT (1 stick)', 2.50, 'TNT, red, single stick');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('TNT2', 1003, 'TNT (5 sticks)', 10, 'TNT, red, pack of 10 sticks');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('FB', 1003, 'Bird seed', 10, 'Large bag (suitable for road runners)');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('FC', 1003, 'Carrots', 2.50, 'Carrots (rabbit hunting season only)');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('SAFE', 1003, 'Safe', 50, 'Safe with combination lock');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('DTNTR', 1003, 'Detonator', 13, 'Detonator (plunger powered), fuses not included');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('JP1000', 1005, 'JetPack 1000', 35, 'JetPack 1000, intended for single use');
    INSERT INTO products(prod_id, vend_id, prod_name, prod_price, prod_desc)
    VALUES('JP2000', 1005, 'JetPack 2000', 55, 'JetPack 2000, multi-use');
    
    
    
    #######################
    # Populate orders table
    #######################
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20005, '2005-09-01', 10001);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20006, '2005-09-12', 10003);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20007, '2005-09-30', 10004);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20008, '2005-10-03', 10005);
    INSERT INTO orders(order_num, order_date, cust_id)
    VALUES(20009, '2005-10-08', 10001);
    
    
    ###########################
    # Populate orderitems table
    ###########################
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 1, 'ANV01', 10, 5.99);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 2, 'ANV02', 3, 9.99);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 3, 'TNT2', 5, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20005, 4, 'FB', 1, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20006, 1, 'JP2000', 1, 55);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20007, 1, 'TNT2', 100, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20008, 1, 'FC', 50, 2.50);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 1, 'FB', 1, 10);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 2, 'OL1', 1, 8.99);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 3, 'SLING', 1, 4.49);
    INSERT INTO orderitems(order_num, order_item, prod_id, quantity, item_price)
    VALUES(20009, 4, 'ANV03', 1, 14.99);
    
    #############################
    # Populate productnotes table
    #############################
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(101, 'TNT2', '2005-08-17',
    'Customer complaint:
    Sticks not individually wrapped, too easy to mistakenly detonate all at once.
    Recommend individual wrapping.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(102, 'OL1', '2005-08-18',
    'Can shipped full, refills not available.
    Need to order new can if refill needed.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(103, 'SAFE', '2005-08-18',
    'Safe is combination locked, combination not provided with safe.
    This is rarely a problem as safes are typically blown up or dropped by customers.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(104, 'FC', '2005-08-19',
    'Quantity varies, sold by the sack load.
    All guaranteed to be bright and orange, and suitable for use as rabbit bait.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(105, 'TNT2', '2005-08-20',
    'Included fuses are short and have been known to detonate too quickly for some customers.
    Longer fuses are available (item FU1) and should be recommended.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(106, 'TNT2', '2005-08-22',
    'Matches not included, recommend purchase of matches or detonator (item DTNTR).'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(107, 'SAFE', '2005-08-23',
    'Please note that no returns will be accepted if safe opened using explosives.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(108, 'ANV01', '2005-08-25',
    'Multiple customer returns, anvils failing to drop fast enough or falling backwards on purchaser. Recommend that customer considers using heavier anvils.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(109, 'ANV03', '2005-09-01',
    'Item is extremely heavy. Designed for dropping, not recommended for use with slings, ropes, pulleys, or tightropes.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(110, 'FC', '2005-09-01',
    'Customer complaint: rabbit has been able to detect trap, food apparently less effective now.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(111, 'SLING', '2005-09-02',
    'Shipped unassembled, requires common tools (including oversized hammer).'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(112, 'SAFE', '2005-09-02',
    'Customer complaint:
    Circular hole in safe floor can apparently be easily cut with handsaw.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(113, 'ANV01', '2005-09-05',
    'Customer complaint:
    Not heavy enough to generate flying stars around head of victim. If being purchased for dropping, recommend ANV02 or ANV03 instead.'
    );
    INSERT INTO productnotes(note_id, prod_id, note_date, note_text)
    VALUES(114, 'SAFE', '2005-09-07',
    'Call from individual trapped in safe plummeting to the ground, suggests an escape hatch be added.
    Comment forwarded to vendor.'
    );

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

  • Hazel,自動整理文件,讓你的 Mac 井井有條

    Hazel,自動整理文件,讓你的 Mac 井井有條

    原文地址

    讓我們從實際需求出發,看看問題出在哪裡,並在此基礎上認識和學習使用 Hazel。

    電腦隨着使用時間的增長,其中的文件也在瘋狂的增長,時間長了也就會出現各種混亂:大量文件堆放在一起,舊文件很少清理,分不清哪些文件還有用,找不到需要的文件等等。

    今天我們就以「下載」和「桌面」為例,聊一聊如何整理我們的電腦。

    Downloads:下載的文件很少處理,時間一長就各種堆積…… 

    Desktop:經常把臨時文件存放在此,方便拖拽使用,但時間一長,就是各種凌亂……

    既然知道了問題所在,那麼我們就來着手整理吧。

    理清整理思路

    首先是確定整理思路,比如如何界定一個文件是否還有用,如何界定它屬於什麼分類等,對應的操作一般是刪除(比如不再需要的或重複的文件)或存檔(學習資料或工作材料等分類存儲),知道如何處理一個文件就很好辦了,剩下的就都是體力活兒。

    雖然這不是一件特別麻煩的事,但是我們也經常忘記或「懶得整理」。這有點類似於打掃房間,當我們沒有時間或者經常忘記時,可以買一台掃地機器人幫助我們打掃,同樣的,在 Mac 上也有這樣一台「機器人」,它就是 Hazel。

    Hazel 是什麼?

    Hazel 是一款可以自動監控並整理文件夾的工具,其官網的介紹就是簡單的一句話:Automated Organization for Your Mac。

    它的使用有點類似於網絡服務 IFTTT,你可以設定一個 if 條件,如果被監控的文件夾出現符合條件的項,那麼對其執行 then 的操作(也可以通過郵箱的收件過濾規則來理解)。

    Hazel 不是一款新工具,它已經有了很長的歷史,其第一個版本在 2006 年底就已經發布,在今年 5 月 4 號,Hazel 發布了 4.0 版本,新增了規則同步(文末會有介紹)、規則搜索等一系列實用功能。

    Hazel 具體能做什麼?

    先為大家簡單羅列一些 Hazel 能做到的事情:

    • 根據文件創建的時間,自動將文件進行顏色標記(比如將最近的文件標記為藍色)
    • 自動的用特定軟件打開某個特定文件(比如下載 BT 種子后,自動用迅雷打開下載)
    • 自動刪除已下載過的 BT 種子文件
    • 根據文件的類型,自動轉移到相應的文件夾中(比如圖片移動到照片文件夾,電影移動到視頻文件夾等)
    • 自動刪除某些特定文件(比如標題中含有固定內容且創建日期在很早以前的)
    • 自動將壓縮文件解壓
    • 自動幫你清理文件的緩存
    • 自動幫你整理照片,可以按照「年 – 月」來分類存儲到相應文件夾
    • 自動把文件夾中的內容上傳到 FTP 等網絡服務中
    • 自動將照片導入 Photos,自動將音樂導入 iTunes 
    • ……

    以上只是列舉的一些場景能夠實現的功能,再加上 Hazel 支持 AppleScript、JavaScript、Automator workflow 等代碼指令,令其擴展性更上一層樓,可以做到的事情也可以說只剩下想象力這道門檻了。

    介紹了不少,下面我們就從 Hazel 的安裝和實際設置來為大家做一個簡單的入門指南。

    Hazel 的安裝

    前往下載最新版本,按照提示安裝,完成后 Hazel 會出現在系統設置中(在應用程序中可找不到哦)。
    Hazel 是一款收費軟件,初次安裝后可以免費試用 14 天,此時可以選擇加載一些簡單的默認規則以幫助你快速上手(當然看完這篇文章也就可以不用加載了)。

    操作后 Hazel 會給我們彈出警告信息:在激活這些規則之前,一定要先檢查它們。具體的方法下面會提及。

    Hazel 的界面和基礎應用

    注:文末提供了文中所有 Hazel 規則的打包下載地址,如果你對文中介紹的規則感興趣,可以直接下載使用。

    Hazel 的主界面包含三部分,分別是設置文件夾規則的 Folders 頁面,設置垃圾箱規則的 Trash 頁面和其他信息頁(Info),今天主要給大家講解文件夾規則設置頁面。

    在 Folders 中包含三部份:設置監控的文件夾(圖中 1),設置該文件夾下的具體規則(圖中 2),設置該文件夾的重複文件處理(圖中 3),圖 1 部分右側的 icon 分別表示「暫停規則執行」和「同步」,建議嘗試新規則的時候先暫停執行再進行調試。

    以整理「下載」文件夾為例,我個人的需求有如下幾條:

    • 最近的下載文件用顏色標籤提醒
    • 超過 3 天的文件不再是新文件,去掉顏色標籤
    • 對存放超過 3 周的文件需進行處理,將滿足此條件的文件用紅色標記提醒
    • 自動刪除已使用的 .torrent 文件
    • 將手機截屏的圖片單獨存放

    上面幾條是梳理自己的整理需求后,選擇的可以被 Hazel 自動執行的。此時回到 Hazel,我們點擊左下角的加號新增「下載」文件夾,隨後在右側 Rules 區域點擊加號新增規則。

    標記最新下載文件

    下圖是規則設置界面,圖 1 部分設置規則名稱和註釋;圖 2 部分設置監控條件,此時設置的是文件添加時間在最後匹配時間之前(新文件添加后暫未被匹配,所以一定是早於匹配時間);圖 3 部分設置執行的動作,此時是將匹配出來的文件標記藍色標籤,並且同時可以被其他規則匹配。

    標記舊文件

    超過 3 天的文件,不再是我需要關注的內容,將其中的藍色標籤去掉:

    標記待處理文件

    對「下載」文件夾,我需要對超過 3 周未處理的文件進行處理,要麼歸檔要麼刪除,需要進行人工判斷的時候我使用紅色標記來提醒自己:

    刪除 .torrent 文件

    在使用 BT 下載之後,留在文件夾的種子文件也就沒有什麼用了,為了防止誤刪設置了 5 天的期限,注意圖中綠色符號,那是點擊了 Preview 后的效果,建議設置規則的時候多使用 Preview 功能來檢查條件設置是否正確,特別是那些複雜的符合條件。

    自動移動手機截屏文件

    工作關係,經常需要在手機上截屏上傳到電腦使用(使用 AirDrop 上傳到「下載」中),這類圖片的處理一般是超過一周后移動到桌面文件夾中再進行集中處理:

    上面介紹了「下載」文件夾的整理思路和執行;對於「桌面」文件夾的整理,我的思路一般是不輕易自動刪除(防誤刪),而是統一到分類文件夾中集中處理。將文檔存放於「文檔」中,將圖片存放於「圖片」中等等,都是非常簡單和基礎的設置,就不做過多介紹;

    下面說一下我對源文件的處理,這裏涉及到條件的嵌套使用:

    圖中使用了嵌套條件,具體的操作是鼠標長按右側加號(也可按住 Option 後點擊),即可增加嵌套條件組。

    附上桌面整理后截圖:

    Hazel 中級應用

    除了以上的基礎使用,Hazel 還可作用於更加廣泛的場景,下面以自動解壓自動清理緩存為例。

    自動解壓

    下載壓縮包后不用手動解壓,Hazel 會自動創建文件夾(按照壓縮包的名稱命名),並將壓縮包和解壓后的文件存放於此:

    有三點需要為大家說明:

    • 設置標籤是為了防止壓縮文件有損壞而導致 Hazel 陷入循環執行中;
    • 不能設置自動刪除,因為 Hazel 會自動選中解壓后的文件,此時的刪除也只是把解壓后的文件刪掉;
    • 使用默認的「Unarchive」操作也可解壓,不過在解壓 .zip 文件後會自動將壓縮包刪掉,所以我這裏使用了第三方的免費解壓軟件  代替(注意:在第一次執行時需要權限設置);不介意刪除壓縮包的同學使用默認的解壓操作即可。

    此規則參考了  的博客,特此感謝。

    自動清理緩存

    以 QQ 為例,QQ 會把群消息中的圖片自動保存到本地,時間一長這個文件夾就很容易達到幾個 G 的大小,這時候 Hazel 又可以派上用處了。

    首先找到你的 QQ 文件夾,可嘗試如下路徑(本人 Mac 系統 10.11)

    /Users/用戶名/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ
    

    將路徑中的「用戶名」換成自己的,然後在 Finder 中按住「⌘ + Shift + G」,把路徑粘貼到輸入框中點擊「前往」即可。

    如果路徑沒問題,就可以在 Hazel 中添加此文件夾了,點擊添加按鈕彈出選擇文件夾界面后,使用上述快捷鍵和路徑同樣可以快速選定,添加後設置如下兩條規則,第一條規則的作用是讓所有子文件夾都可以適配規則並執行操作;第二條規則是把超過 500M 的子文件夾進行刪除操作,且不會直接刪除父文件夾。

    至此,QQ 緩存文件的自動清理就設置完成了,其他軟件緩存也可以進行類似的規則設計,不過一定要注意確保這裏面沒有你需要的文件,否則一旦刪除要找回也是頗為麻煩的。

    更多用法

    如前文所說,Hazel 能做到的不止這些場景,還有用戶用它來整理照片,利用 AppleScript 執行更加複雜的工作流程等等,這裏僅當作拋磚引玉,歡迎大家分享自己的用法,並且以後也會有更多關於 Hazel 使用技巧的文章。 

    其他功能

    管理垃圾箱

    在 Hazel 的 Trash 頁面,可以進行一些垃圾箱的設置,比如將其中超過一周的文件刪除,保持垃圾箱大小控制在 2GB 左右,選擇刪除時是否使用安全刪除功能,以及卸載應用時檢測其附屬文件夾等等;這方面的功能筆者並不常用,在此不做過多介紹。

    刪除應用時檢測相關文件,並可選擇一併刪除。作用類似於 。

    同步規則

    同步功能在 4.0 終於推出,現在也可以方便的使用在多台電腦上了。點擊左側面板中的齒輪圖標,選擇 Rule Sync Options 即可打開同步界面(也可在文件夾上右鍵選擇 Rule Sync Options)。

    同步需要配合第三方同步網盤使用,當前文件夾若是第一次使用同步,需要設置同步文件存放路徑,點擊 Set up new sync file 即可。如果要使用同步的文件,在界面中點擊 Use existing sync file 即可。

    Hazel 的下載

    Hazel 是一款收費軟件(),五月初的時候發布了 4.0 版本,單獨購買是 $32,Family Pack $49,從 3.0 版本升級需要 $10。初次下載可以免費試用 14 天,建議大家先試用再購買。

    最後給大家提供我自己的 Hazel 設置,你可以導入后調整為適合自己的規則再使用:。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • PowerMock學習(一)之PoweMock的入門

    PowerMock學習(一)之PoweMock的入門

    關於powermock

    在TDD領域Mock框架有很多,比如EasyMock,JMock,Mockito。可能有些同學會好奇了,為什麼要重點把powermock拿出來呢,因為powermock可以解決前面三種框架不能解決的問題,而且powermock也是是單元測試中極其強大的測試框架。

    powermock特點

    • 主要圍繞着Junit、TestNg測試框架開展進行
    • 對所依賴的Jar包非常的苛刻,出現jar包的衝突或者不一致就不能使用
    • PowerMock也是一種Mock,主要是解決其他Mock不能解決的問題,通俗的講,就是專治各種不服

    powermock入門實例

    1、引入依賴jar包

    <dependency>
                <groupId>org.powermock</groupId>
                <artifactId>powermock-module-junit4</artifactId>
                <version>1.6.1</version>
                <scope>compile</scope>
            </dependency>
            <dependency>
                <groupId>org.powermock</groupId>
                <artifactId>powermock-api-mockito</artifactId>
                <version>1.6.1</version>
                <scope>compile</scope>
            </dependency>

     

    2、實際案例

    模擬場景:新增學生操作

    先建一個名為StudentService的類,用來模擬服務調用操作,在這個類中新增一個方法,來模擬查詢總共有多少個學生。

    具體示例代碼如下:

    package com.rongrong.powermock.service;
    
    import com.rongrong.powermock.dao.StudentDao;
    
    /**
     * @author rongrong
     * @version 1.0
     * @date 2019/11/17 21:13
     */
    public class StudentService {
        private StudentDao studentDao;
    
        public StudentService(StudentDao studentDao) {
            this.studentDao = studentDao;
        }
    
        /**
         * 獲取學生個數
         *
         * @param studentDao
         */
        public int getTotal(StudentDao studentDao) {
            return studentDao.getTotal();
        }
    }

    可以看出創建service需要傳遞StudentDao這個類,接着我們再來創建StudentDao這個類,用於進行新增操作。

    具體示例代碼如下:

    package com.rongrong.powermock.dao;
    
    /**
     * @author rongrong
     * @version 1.0
     * @date 2019/11/17 21:15
     */
    public class StudentDao {
        public int getTotal() {
            throw new UnsupportedOperationException();
        }
    }

     

    仔細看,你會發現,你肯定調不了dao了,這回傻了吧,哈哈哈!!!

    你會好奇這塊為啥我要拋出UnsupportedOperationException異常呢,因為我就想模擬服務不可用的情況(實際中經常會遇到可能由於某種原因(沒有完成,或者資源不存在等)無法為 Service 服務),這樣的情況,難道我們就不測試了嗎?

    那我還是乖乖的把測試用例寫完,並測試下吧,下面我們再來創建一個名為StudentServiceTest的測試類。

    具體示例代碼如下:

    package com.rongrong.powermock.service;
    
    import com.rongrong.powermock.dao.StudentDao;
    import org.testng.annotations.Test;
    
    /**
     * @author rongrong
     * @version 1.0
     * @date 2019/11/17 21:19
     */
    public class StudentServiceTest {
        @Test
        public void testAddStudent() {
            StudentDao studentDao = new StudentDao();
            StudentService studentService = new StudentService(studentDao);
            studentService.getTotal(studentDao);
        }
    
    }

     

    上面的測試用例肯定會執行失敗,那我們也來執行下看,效果如下圖:

     

    我們先將這個報錯,腦補為鏈接不上數據庫,問題很明顯,數據庫掛了,就是連接不上了,等着服務器好了得三天後,可是今晚領導就要看功能實現,你該怎麼辦?無法測試service,難道就真的結束了嗎?
    答案是否定的,此時我們用powermock便可完美解決問題,接下來我們請出powermock登場。

    具體代碼如下:

    package com.rongrong.powermock.service;
    
    import com.rongrong.powermock.dao.StudentDao;
    import org.powermock.api.mockito.PowerMockito;
    import org.testng.Assert;
    import org.testng.annotations.Test;
    
    /**
     * @author rongrong
     * @version 1.0
     * @date 2019/11/17 21:19
     */
    public class StudentServiceTest {
    
        @Test
        public void testGetStudentTotal() {
            StudentDao studentDao = PowerMockito.mock(StudentDao.class);
            PowerMockito.when(studentDao.getTotal()).thenReturn(666);
            StudentService studentService = new StudentService(studentDao);
            int total = studentService.getTotal(studentDao);
            Assert.assertEquals(total, 666);
        }
    
    
    }

     

    這時再次運行,你會發現神奇般的運行通過,結果如下圖所示:

     

    是不是很神奇,很驚喜,沒錯,這個框架就是這麼強大。

    我們可以這樣理解mock就是創建一個假的該對象,然後需要你告訴這個對象調用某個方法的時候返回某個你指定的值即可。

    到此,一個簡單powermock入門結束,如您覺得好,請繼續關注我,謝謝支持!

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 比亞迪計畫在廣州增城設廠 組裝K9電動大巴

    日前,有媒體報導稱,比亞迪高層與廣州市長洽談在廣州增城投資建廠事宜,屆時增城將雲集廣汽本田、北汽和比亞迪三大汽車品牌,成為廣州汽車產業最為集中的區域。

    比亞迪向媒體確認,其總裁王傳福及高級副總裁吳經勝此前一段時間的確曾赴廣州,與廣州市市長陳建華洽談在廣州投資建廠事宜。此項目與比亞迪在陝西、雲南等地的模式類似,依舊是為其公交電動化做準備。目前比亞迪仍在積極籌備還有天津、昆明、武漢等地的建廠項目,也是其在新能源推廣的地方保護背景下的無奈之舉。

    比亞迪的電動大巴在國內具有較強的優勢,目前已經出口到荷蘭、巴西、美國等地,目前在國內由於地方保護,只有在當地合資設廠,是打入當地市場的一個有效市場。而電動大巴的組裝線投資不大,也是比亞迪較能接受的方式。

    為了推動電動車的發佈,比亞迪率先推出了“零元購車、零成本、零排放”城市公交電動化解決方案,為加速公交電動化進程開闢一條現實可行的道路。在深圳、西安、寶雞、韶關、荷蘭、新加坡、美國、丹麥、德國、英國倫敦等地成功實現電動車的規模化、商業化運營。

    比亞迪總部所在廣州運行的電動公交大巴超過千輛,成為電動大巴運行最多的城市。而百公里之外的廣州,目前擁有超過1萬輛的公交大巴,其中新能源車為2000輛,少數為電動大巴,主要廣汽客車品牌;計程車約有3萬輛,電動車計程車幾乎為零,這給比亞迪很大的期望值。

    比亞迪也在做廣州的工作。在2012廣州國際馬拉松賽上,作為本屆馬拉松比賽獨家汽車贊助商,比亞迪旗下的城市多功能SUV車型S6、T動力智慧新典範G6以及純電動車e6成為賽事的指定用車。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

  • 北汽新能源全新戰略:十三五末期產能達80萬輛以上

    在產業佈局方面,北汽新能源將以“1(北京采育基地)+2(常州基地、青島基地)+I(北汽集團內部傳統乘用車生產基地)+P(社會合作夥伴生產資源)>80”為基礎,在十三五末期形成80萬輛以上生產能力,年產銷50萬輛規模,打造年營業收入600億元、上市市值1000億元的企業。

    在研發方面,北汽新能源將推進“1496”戰略,1:打造1個世界級科技創新中心,具備正向開發能力;4:構建4層次研發體系;9:在研發總部建成9大研發中心;5:整合全球資源,組建5大海外研發中心。此外,在北汽新能源的目標中,其還將爭創新能源汽車行業最佳雇主品牌,價值鏈研發團隊人數達到5000以上,專業領域國際級人才占比達到10%以上,經營團隊中具有國際視野的複合型人才占比60%以上。

    產品方面,北汽新能源將構建3大技術平臺(共用平臺、協同平臺、全新平臺),同樣將實現3大維度(大中小、高中低、234)全面發展,打造三款年銷量突破10萬輛的明星車型。基於國內當前新能源汽車市場發展特點,北汽新能源短期內聚焦A級以下車型,並根據市場發展適時啟動B、C級高端車型儲備開發。

    今年,北汽新能源將推出首款純電動6萬元高性價比國民純電動車也將在2016年迎來投放,實現不靠地方補貼國內市場全覆蓋。2017年,公司將推出採用全鋁框架超輕量化車身設計的EX系列純電動精品微型小車。

    根據規劃,北汽新能源未來每年將研發4-6款新車型,形成“2、3、4”(即續航里程200公里、300公里、400公里)、“高、中、低”(即高檔、中檔、標配三個級別)、“大、中、小”(即車體大小)完整新能源汽車產品組合,為消費者提供更加豐富的選擇。

    密集的產品投放加上車型續航能力不斷升級,這些利好都將為北汽新能源持續保持國內純電動市場第一、全球前四的發展目標奠定堅實基礎。而在擴張產能的同時,有效利用起北汽集團以及合作夥伴的資源,則可以大幅縮減開支,獲得更高的利潤。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 財政部等五部委發佈十三五新能源汽車充電基礎設施獎勵政策

    1月19日從財政部獲悉,財政部、科技部、工業和資訊化部、發展改革委、國家能源局等五部委聯合發佈《關於“十三五”新能源汽車充電基礎設施獎勵政策及加強新能源汽車推廣應用的通知》,旨在加快推動新能源汽車充電基礎設施建設,培育良好的新能源汽車應用環境,2016-2020年中央財政將繼續安排資金對充電基礎設施建設、運營給予獎補。

    通知規定獎補物件,中央財政充電基礎設施建設運營獎補資金是對充電基礎設施配套較為完善、新能源汽車推廣應用規模較大的省(區、市)政府的綜合獎補。

    通知確定獎勵資金使用範圍,獎補資金應當專門用於支持充電設施建設運營、改造升級、充換電服務網路運營監控系統建設等相關領域。地方應充分利用財政資金杠杆作用,調動包括政府機關、街道辦事處和居委會、充電設施建設和運營企業、物業服務等在內的相關各方積極性,對率先開展充電設施建設運營、改造升級、解決充電難題的單位給予適當獎補,並優先用於支援《國務院辦公廳關於加快電動汽車充電基礎設施建設的指導意見》確定的相關重點任務。

    通知設定新能源充電設施獎勵標準,對於大氣污染治理重點省市獎勵最高,2016年大氣污染治理重點省市推廣量3萬輛,獎補標準9000萬元,超出門檻部分獎補最高封頂1.2億元。2020年大氣污染治理重點省市獎勵門檻7萬輛,獎補標準1.26億元。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • Jetpack Compse 實戰 —— 全新的開發體驗

    Jetpack Compse 實戰 —— 全新的開發體驗

    公眾號回復 Compose 獲取安裝包

    項目地址:

    經過前段時間的 Android Dev Summit ,相信你已經大概了解了 Jetpack Compose 。如果你還沒有聽說過,可以閱讀這篇文章 。總而言之,Compose 是一個 顛覆性聲明式 UI 框架 ,它的口號就是 消滅 xml 文件 !

    儘管 Jetpack Compose 還只是預覽版,API 可能發生變化,缺乏足夠的控件支持,甚至不是那麼穩定,但這阻止不了我這顆好奇的心。我在第一時間就上手擼了一款 Compose 版本 Wanandroid 應用,功能也比較簡單,僅僅包括首頁,廣告和最新項目,類似於 Android 原生頁面的 Viewpager + TabLayout 。下面的 gif 展示了應用的基本頁面:

    可以看出來頁面並不是那麼流暢,View 的復用應該是個問題,甚至我也沒發現應該怎麼做下拉刷新。那麼,Compose 給我們帶來了什麼呢?在解答這個問題之前,我想先來說說 Android 應用架構問題。

    荒蕪年代 —— MVC

    在我剛入行的時候,可以說是 Android 開發的黃金時代,也可以說是開發者的荒蕪時代。一方面,毫不誇張的說,基本會寫 xml 都能謀得一份工作。另一方面,對於開發者來說,遠遠沒有現在的規範的開發架構。沒記錯的話,我當年的主力開發框架是 xUtils 2 ,一個類庫大包干,從 布局和 ID 綁定,網絡請求,到圖片展示,ORM 操作,一應俱全。當時的 布局和 ID 綁定還是運行時反射,而不是編譯期註解。很長一段時間以來,Android 連一個官方的網絡庫都沒有。

    在架構方面,很多人都是一個 Activity 擼到死,我真的見過上千行zouguolai的 MainActivity 。並且覺得這就是 MVC 架構,實體類 Entity 就是 Model 層,Activity/Fragment 就是 Controller 層,布局文件就是 View 層。

    但這當真是 MVC 嗎?其實並不是。不管是 MVCMVP,還是 MVVM,都應該遵循一個最起碼的原則,表現層和業務層分離 ,也就是 Android 官網給出的 中強調的 分離關注點Activity/Fragment 既要承擔視圖層的任務,展示和更新 UI,又要處理業務層邏輯,獲取數據等。這並不符合架構設計的基本原則。

    正確的 MVC 模式中,Model 層不僅包含實體類 Entity,更重要的作用是處理業務邏輯。View 層負責處理視圖邏輯。而 Controller 就是 Model 和 View 之間的橋樑。橋怎麼建,其實並沒有標準,根據你自己的需求就可以了。

    引用一張阮一峰老師的圖,大致是這麼個意思,但是也不一定就完全都是單向依賴。

    從荒蕪時代走過來,MVC 總算有點分層的味道在裏面了,分離了視圖層和業務層。但是 View 層和 Model 層的依賴關係,造成代碼耦合,終將導致 Activity 日益臃腫。那麼有沒有辦法將 View 層和 Model 層徹底分離,做到視圖層和模型層完全分離呢? MVP 就應運而生了。

    青銅年代 —— MVP

    依舊是阮一峰老師的圖片:

    相較於 MVC ,MVP 用 Presenter 層 代替了 Controller 層 ,且 View 層Model 層 完全分離,依靠 Presenter 進行通信 。

    想象一個獲取用戶信息的場景。IView 接口中定義了一系列視圖層接口 ,View 層(Activity)實現 IView 接口中相應視圖邏輯。 View 層通過持有的 Presenter 處理業務邏輯,即請求用戶信息。一般情況下,Presenter 也不直接處理業務邏輯,而是通過 Model 層,例如數據倉庫 Repository, 來獲取數據,避免 Presenter 重蹈覆轍,日漸臃腫。同時,Presenter 層也是持有 VIew 的,獲取用戶信息之後再轉發給 View 。

    總結一下,MVP 中 View 和 Model 完全解耦,通過 Presenter 通信。View 和 Presenter 共同處理視圖層邏輯,Model 層負責業務邏輯。

    在 Github 上 Android 官方的架構示例 中 MVP 作為主分支堅挺了很久。我最初也是根據這個官方示例改造了自己的 MVP 架構,並且使用了很長時間。但是 MVP 作為一款面向接口編程的架構,隨着業務的複雜程度不斷加大,有種遍地都是接口的既視感,實在顯得有點繁瑣。

    另外一點,Presenter 的職責邊界不夠清晰,它除了承擔調用 Model 層獲取業務邏輯之外,還要控制 View 層處理 UI。用下面一段代碼錶示一下:

    class LoginPresenter(private val mView: LoginContract.View) : LoginContract.Presenter {
    
        ......
    
        override fun login(userName: String, passWord: String) {
            CoroutineScope(Dispatchers.Main).launch {
                val result = WanRetrofitClient.service.login(userName, passWord).await()
                with(result) {
                    if (errorCode == -1)  mView.loginError(errorMsg) else mView.login(data)
                }
            }
        }
    }

    一旦 View 層發生任何變化,Presenter 層也要做出相應改動。雖然 View 和 Model 之間解耦了,但是 View 和 Presenter 卻耦合了。理想情況下,Presenter 層應該僅負責數據的獲取,View 層自動觀察數據的變化。於是,MVVM 來了。

    黃金時代 —— MVVM

    Google 官圖鎮樓 。

    MVP 風光早已不在, Android 官方的架構示例 的主分支已經切換到 MVVM 。在 Android 的 MVVM 架構中,ViewModel 是重中之重,它一方面通過數據倉庫 Repository 獲取數據,另一方面根據獲取的數據更新 View 層的 Activity/Fragment。等等,這句話怎麼聽着這麼耳熟,Presenter 不也是幹了這些事嗎?的確,它們乾的事情都差不多,但是實現上完全不一樣。

    以我的開源項目 中的 LoginViewModel 為例:

    class LoginViewModel(val repository: LoginRepository) : BaseViewModel() {
    
        private val _uiState = MutableLiveData<LoginUiModel>()
        val uiState: LiveData<LoginUiModel>
            get() = _uiState
    
    
        fun loginDataChanged(userName: String, passWord: String) {
            emitUiState(enableLoginButton = isInputValid(userName, passWord))
        }
    
        // ViewModel 只處理視圖邏輯,數據倉庫 Repository 負責業務邏輯
        fun login(userName: String, passWord: String) {
            viewModelScope.launch(Dispatchers.Default) {
                if (userName.isBlank() || passWord.isBlank()) return@launch
    
                withContext(Dispatchers.Main) { showLoading() }
    
                val result = repository.login(userName, passWord)
    
                withContext(Dispatchers.Main) {
                    if (result is Result.Success) {
                        emitUiState(showSuccess = result.data,enableLoginButton = true)
                    } else if (result is Result.Error) {
                        emitUiState(showError = result.exception.message,enableLoginButton = true)
                    }
                }
            }
        }
    
        private fun showLoading() {
            emitUiState(true)
        }
    
        private fun emitUiState(
                showProgress: Boolean = false,
                showError: String? = null,
                showSuccess: User? = null,
                enableLoginButton: Boolean = false,
                needLogin: Boolean = false
        ) {
            val uiModel = LoginUiModel(showProgress, showError, showSuccess, enableLoginButton,needLogin)
            _uiState.value = uiModel
        }
    
        data class LoginUiModel(
                val showProgress: Boolean,
                val showError: String?,
                val showSuccess: User?,
                val enableLoginButton: Boolean,
                val needLogin:Boolean
        )
    }

    可以看到,ViewModel 中是沒有 View 的引用的,View 通過可觀察的 LIveData 來觀察數據變化,基於觀察者模式做到和 ViewModel 完全解耦。

    數據驅動視圖 ,這是 Jetpack MVVM 推崇的一個重要原則。其基本數據流如下所示 :

    • 數據層 Repository 負責從不同數據源獲取和整合數據,基本負責所有的業務邏輯
    • ViewModel 持有 Repository,獲取數據並驅動 View 層更新
    • View 持有 ViewModel,觀察 LiveData 攜帶的數據,數據驅動 UI

    曾經和一些開發者討論過這樣一個問題,** 不使用 DataBinding 還算是 MVVM 嗎 ?** 我認為 MVVM 的核心從來不在於 DataBinding 。DataBinding 只是可以幫助我們將 數據驅動視圖 做到極致,順便還可以雙向綁定。

    要說到對 Jetpack MVVM 中最不滿意的一塊,那非 DataBinding 莫屬了。在我狹隘的認為 DataBinding 就是一個在 xml 裏面寫邏輯代碼的反人類的庫時,我是堅決反對在任何項目中引入它的。固執己見的時候就容易走進誤區,在閱讀 KunminX 的 之後,正如這篇文章名字一樣,真香。

    香的確是香,一切能讓我早下班的都是好東西。在我的某次提交日誌上,我寫下了 消滅 Adapter 幾個字,那時我剛用 DataBinding 消滅了大部分 RecyclerView 的 Adapter 。可是在提交之後,我的良心惴惴不安,我追究還是在 xml 文件里寫邏輯代碼了,難道這真的不反人類嗎?

    未來可期 —— Jetpack Compose

    現在你應該可以理解我對 Jetpack Compose 的執念了。拋去其他特性,在我看來,它完美的解決了 數據驅動視圖 的問題,我再也不需要使用 DataBinding 了。

    簡單代碼展示一下 Compose 的用法。下面的代碼描繪的是首頁 Tab 下的文章列表。

    @Composable
    fun MainTab(articleUiModel: ArticleViewModel.ArticleUiModel?) {
    
        VerticalScroller {
            FlexColumn {
                inflexible {
                    HeightSpacer(height = 16.dp)
                }
                flexible(1f) {
                    articleUiModel?.showSuccess?.datas?.forEach {
                        ArticleItem(article = it)
                    }
    
                    articleUiModel?.showError?.let { toast(App.CONTEXT, it) }
    wenjian
                    articleUiModel?.showLoading?.let { Progress() }
                }
            }
        }
    }

    這種寫法叫做 聲明式編程 ,會用 Flutter 的同學應該很熟悉。方法參數 ArticleUiModel 就是數據實體類,直接根據數據 ArticleUiModel 構建 UI 。說的大白話一點,就是給你長方形的長和寬了,讓你畫個長方形出來。最後加上 @Compose 註解,就是一個可用的 UI 組件了。仔細看代碼,裏面還用了兩個 UI 組件 ,ArticleItemProgress ,代碼就不貼出來了。分別是文章列表的 item 項目 和加載進度條。

    那麼,數據如何更新呢?最簡單的方式是使用 @Model 註解。

    @Model
    data class ArticleUiModel(){
      ......
    }

    對,就是這麼簡單。@Model 註解會自動把你的數據類變成可觀察對象,只要 ArticleUIModel 發生變化,UI 就會自動更新。

    但是我在實際開發中結合 LiveData 使用時,好像表現的不是那麼正常。後來在 Medium 上無意中看到了解決方案,針對 LiveData 做了特殊處理 :

    // general purpose observe effect. this will likely be provided by LiveData. effect API for
    // compose will also simplify soon.
    fun <T> observe(data: LiveData<T>) = effectOf<T?> {
        val result = +state<T?> { data.value }
        val observer = +memo { Observer<T> { result.value = it } }
    
        +onCommit(data) {
            data.observeForever(observer)
            onDispose { data.removeObserver(observer) }
        }
    
        result.value
    }wenjian

    在 Activity/Fragment 中觀測 LiveData 即可:

    class MainActivity : BaseVMActivity<ArticleViewModel>() {
    
        override fun initView() {
            setContent {
               +observe(mViewModel.uiState)
                WanandroidApp(mViewModel)
            }
        }
    
        override fun initData() {
           mViewModel.getHomeArticleList()
        }
    }

    這樣 View 層就可以自動觀察 LiveData 所包含的值了。

    沒有 xml,沒有 DataBinding,一切看起來稱心如意多了。但就是 UI 體驗有那麼一點糟心,你可以在公眾號後台回復 Compose 安裝體驗一下。由於還是早期的預覽版,這也是可以理解的。我相信,等到發布 Release 版本的時候,一定足以完全代替原聲的 View 體系。

    本文並沒有詳細介紹 Jetpack Compose 的詳細使用過程和其他特性,更多信息我推薦下面兩篇文章:

    最後

    正如 Android 官網 Jetpack 介紹頁所說,Jetpack 可以幫助開發者更輕鬆的編寫優質應用。的確,隨着應用架構的規範,我們只需要把精力放在需要的代碼上,加速開發,消除樣板代碼,減少崩潰和內存泄露,構建高質量的強大應用。我想不出來有任何理由不使用 Jetpack 來構建你的應用。而 Compose 必將稱為 Jetpack 中極其重要的一塊拼圖。

    Jetpack Compse ,未來可期 !

    添加我的微信,加入技術交流群。

    公眾號後台回復 “compose”, 獲取最新安裝包。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

  • 必知必會-存儲器層次結構

    必知必會-存儲器層次結構

    相信大家一定都用過各種存儲技術,比如mysql,mongodb,redis,mq等,這些存儲服務性能有非常大的區別,其中之一就是底層使用的存儲設備不同。作為一個程序員,你需要理解存儲器的層次結構,這樣才能對程序的性能差別瞭然於心。今天帶大家了解下計算機系統存儲器的層次結構。

    存儲技術

    首先了解下什麼是存儲器系統?

    實質上就是一個具有不同容量、成本和訪問時間的存儲設備的層次結構。從快到慢依次為:CPU寄存器、高速緩存、主存、磁盤;

    這裏給大家介紹一組數據,讓大家有一個更清晰的認識:

    如果數據存儲在CPU寄存器,需要0個時鐘周期就能訪問到,存儲在高速緩存中需要4~75個時鐘周期。如果存儲在主存需要上百個周期,而如果存儲在磁盤上,大約需要幾千萬個周期! — 出自 CSAPP

    接下來一起深入了解下計算機系統涉及的幾個存儲設備:

    隨機訪問存儲器

    隨機訪問存儲器(RAM)分為靜態RAM (SRAM) 和動態RAM(DRAM)。SRAM的速度更快,但也貴很多,一般不會超過幾兆字節,通常用來做告訴緩存存儲器。DRAM就是就是我們常說的主存。

    訪問主存

    數據流是通過操作系統中的總線的共享电子電路在處理器和DRAM之間來來回回。每次CPU和主存之間的數據傳送都是通過一系列複雜的步驟完成,這些步驟成為總線事務。讀事務是將主存傳送數據到CPU。寫事務從CPU傳送數據到主存。

    總線是一組并行的導線,能攜帶地址、數據和控制信號。下圖展示了CPU芯片是如何與主存DRAM連接的。

    那麼我們在加載數據和存儲數據時,CPU和主存到底是怎樣交互實現的呢?

    首先來看一個基本指令,加載內存數據到CPU寄存器中:

    movq A,%rax

    將地址A的內容加載到寄存器%rax中,這個命令會使CPU芯片上稱為總線接口(bus interface)的電路在總線上發起讀事務,具體分為三個步驟:

    1. CPU將地址A放到系統總線上,I/O橋將信號傳遞到內存總線。詳情看下下圖a
    2. 主存感覺到內存總線上的地址信號,從內存總線讀地址,從DRAM取出數據字,將其寫到內存總線。I/O橋將內存總線信號翻譯成系統總線信號,沿着系統總線傳遞到CPU總線接口。下圖b
    3. CPU感覺到系統總線上的數據,從總線上讀數據,並將數據複製到寄存器%rax。下圖c

    隨機訪問存儲器,有個缺點是當斷電后,DRAM和SRAM會丟失它們的信息,因此為易失性存儲。

    磁盤存儲

    磁盤是廣為使用的保存大量數據的存儲設備,目前我們家用電腦,動輒也都是1T的。它相比於基於RAM的只有幾百或幾千兆字節的存儲器來說,雖然大但是讀寫性能差。時間為毫秒級,比DRAM讀慢了10萬倍,比SRAM慢了100萬倍。

    磁盤構造

    磁盤是由盤片構成的。每個盤片有兩面。表面覆蓋著磁性記錄材料。盤片中央是一個可以旋轉的主軸(spindle),它使盤片可以以固定的速率旋轉,通常是5400~15000轉每分鐘,磁盤通常包含多個盤片,密封在一個容器內。

    如上圖,我們可以看到,表面被劃分為很多同心圓,稱為磁道。磁道又被劃分為很多扇區,每個扇區具有相同的數據位(通常512字節)。扇區之間有間隙隔開,用來存儲標識扇區的格式化位。

    多個盤片封裝在一起到一個容器中,就是我們平時用的硬盤,稱為磁盤驅動器。

    磁盤容量

    容量很好理解,就是磁盤一共可以存儲的數據位。根據磁盤的構造,我們得出磁盤的容量由下面因素決定:

    • 記錄密度(recording density,位/英寸):磁道一英寸可以放入的位數。
    • 磁道密度(track density,道/英寸):從中心主軸向外的半徑上,一英寸可以有多少磁道。
    • 面密度(areal density,位/平方英寸):記錄密度與磁道密度的乘積。

    通過上面的了解,增加磁盤容量其實就是增加面密度,近些年面密度每隔幾年就會翻倍。下面大家可以看一下這個磁盤容量的計算公式:

    磁盤容量=字節數/扇區 * 平均扇區數/磁道 * 磁道數/表面 * 表面數/盤片 * 盤片數/磁盤

    結合一個例子方便各位理解:

    假如我們有一個磁盤,有5個盤片,每個扇區512字節,沒個面20000條磁道,每條磁道 300 個扇區,那麼容量計算為:

    磁盤容量 = 512 * 300 * 20000 * 2 * 5 = 30720000000字節=30.72G

    磁盤操作

    磁盤讀寫操作靠的是讀寫頭來讀寫存儲在磁性表面的位,它在傳動臂的一端,通過這個傳動臂沿着半徑前後移動,從而讀取不同的磁盤上數據,這個過程就成為尋道(seek)

    通過上圖可以清晰的了解到,在讀取數據的時候,首先通過傳動臂沿着半徑將讀寫頭移動到對應表面的磁道上,而表面一直在以固定的速率旋轉,讀取指定扇區的數據(磁盤是以扇區大小來讀寫數據)。因為對於數據訪問來說,消耗時間主要集中在:尋道時間、旋轉時間和傳送時間。

    • 尋道時間:即移動傳動臂到包含目標扇區的磁道上所需的時間;
    • 旋轉時間:即尋道完成后,等待目標扇區的第一個位旋轉到讀寫頭下的時間;
    • 傳送時間:即扇區第一個位開始位於讀寫頭下,到最後一個位所需的時間;

    這裏給出一個書上寫的結論,訪問一個磁盤扇區中512字節的時間主要是尋道時間和旋轉延遲。也就是訪問扇區中第一個字節花費很長時間,剩下的幾乎不用時間。

    這裏大家可能有疑問,CPU是如何讀取磁盤的數據到主存的,這就需要了解I/O總線。他們通過多種適配器連接到總線,而I/O總線連接了內存和CPU。如下圖所示:

    也就是I/O總線連接各種I/O設備、主存等。

    固態硬盤

    固態硬盤也就是俗稱的SSD(Solid State Disk),是一種基於閃存的存儲技術,目前常用的日常PC都用它來代替了磁盤,獲取更快的速度。

    SSD是內部由閃存構成,一個閃存由B個塊的序列組成,每個塊由P頁組成。通常頁的大小是512字節~4KB,塊由32~128頁組成,塊的大小為16KB~512KB。

    SSD的隨機讀比寫快很多,是因為:

    1. 在寫的時候,只有一頁所屬的整個塊被擦除之後才能寫。而擦除塊需要較長時間,1ms級的,比讀取高一個數量級。
    2. 如果寫的頁P已經有數據,那麼這個塊中所有帶數據的頁都必須被複制到一個新的已經擦除過的塊,然後才能對頁P寫操作。

    在大約進行100000次重複寫之後,塊會被磨損,不能在使用,所以這也是網上建議保存固態磁盤不要頻繁格式化,作為系統盤的原因。

    局部性

    現在計算機頻繁的使用基於SRAM的告訴緩存,為了彌補處理器-內存之間的差距,這種方法行之有效是因為局部性這個基本屬性。

    程序的局部性原理是指程序在執行時呈現出局部性規律,即在一段時間內,整個程序的執行僅限於程序中的某一部分。相應地,執行所訪問的存儲空間也局限於某個內存區域。局部性原理又表現為:時間局部性和空間局部性。時間局部性是指如果程序中的某條指令一旦執行,則不久之後該指令可能再次被執行;如果某數據被訪問,則不久之後該數據可能再次被訪問。空間局部性是指一旦程序訪問了某個存儲單元,則不久之後。其附近的存儲單元也將被訪問。

    上面我們介紹了內存和磁盤的讀取邏輯,因此一旦某個數據被訪問過,很快的時間內再次被訪問,則會有緩存等手段,提高訪問效率。

    因此我們程序中應該尊村下列普遍方法:

    1. 重複引用相同變量的程序有良好的時間局部性;
    2. 總是順序訪問數據,跨越的步長越小,則程序的空間局部性越好。
    3. 對於取指令來說,循環有好的時間和空間局部性。循環體越小,循環迭代次數越多,局部性越好。

    比如一個for循環,這是平時經常使用到的場景。假設它訪問一個同一個數組元素,那麼這個數組就是當前階段的訪問工作集,在緩存夠大的情況下,它是可以直接命中緩存的。

    存儲器層次結構

    上面主要介紹了存儲技術和計算機軟件一些基本的和持久的屬性:

    • 存儲技術:不同的存儲技術的訪問時間差異很大。速度較快的技術每字節的成本要比速度慢技術高,而且容量越小。CPU和主存之間的速度差距在增大;
    • 計算機軟件:一個便攜良好的程序傾向於展示出良好的局部性。

    而現在計算機系統中,硬件和軟件這些基本屬性互相補充的很完美,即高層從底層走,存儲設備變得更慢、更便宜和更大,頂層的是CPU寄存器,CPU可以在一個時鐘周期內訪問他們,接下來是高速緩存SRAM、主存等 。

    看上圖所示,其中心思想就是:對於每個k,位於k層的更快更小的存儲設備是作為位於k+1層更大更慢設備的緩存。

    概括來說,基於緩存的存儲器層次結構行之有效,因為較慢的存儲設備比較快的設備更便宜,還因為程序傾向於展示局部性。

    • 利用時間局部性:由於時間局部性,同一數據可能會被多次使用,在第一次使用緩存不命中后就被複制到緩存中,後面在訪問時性能就比第一次快很多。
    • 利用空間局部性:存儲設備底層都有塊的概念,作為基本的讀取單位。通常塊包含多個數據,由於空間局部性,後面對該塊中其他對象的訪問即命中緩存,彌補首次訪問塊複製的消耗;

    總結

    今天,這篇文章主要學習了計算機存儲器的相關知識。

    1. 常用的存儲技術,以及計算機是如何操作這些存儲設備中的數據的。
    2. 講解了程序中的局部性原理,時間局部性和空間局部性。方便大家寫出更快的程序。
    3. 最後學習了整個計算機系統的存儲器層次結構。存儲系統其實就是一個多級緩存系統,上層的存儲設備昂貴,容量小,價格貴,但是速度快,作為下一層設備的緩存。

    閱讀更多內容,請瀏覽我的個人小站:

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!