HashMap7淺析

一、概述

  HashMap,基於哈希結構的Map接口的一個實現,無序,允許null鍵值對,線程不安全的。可以使用集合工具類Collections中的synchronizedMap方法,去創建一個線程安全的集合map。

  在jdk1.7中,HashMap主要是基於 數組+鏈表 的結構實現的。鏈表的存在主要是解決 hash 衝突而存在的。插入數據的時候,計算key的hash值,取得存儲的數組下標,如果衝突已有元素,則會在衝突地址上生成個鏈表,再通過key的比較,鏈表是否已存在,存在則覆蓋,不存在則鏈表上添加。這種方式,如果存在大量衝突的時候,會導致鏈表過長,那麼直接導致的就是犧牲了查詢和添加的效率。所以在jdk1.8版本之後,使用的就是 數組 + 鏈表 + 紅黑樹,當鏈表長度超過 8(實際加上初始的節點,整個有效長度是 9) 的時候,轉為紅黑樹存儲。

  本文中內容,主要基於jdk1.7版本,單線程環境下使用的HahsMap沒有啥問題,但是當在多線程下使用的時候,則可能會出現併發異常,具體表象是CPU會直線上升100%。下面是主要介紹相關的存取以及為什麼會出現線程安全性問題。

二、結構

  

  HashMap默認初始化size=16的哈希數組,然後通過計算待存儲的key的hash值,去計算得到哈希數組的下標值,然後放入鏈表中(新增節點或更新)。鏈表的存在即是解決hash衝突的。

三、源碼實現分析

  1、存儲具體數據的table數組:

      

    Entry為HashMap中的靜態內部類,其具體結構如下圖

      

    key、value屬性就是存儲鍵值對的,next則是指向鏈表的下一個元素節點。

     2、 默認初始化方法:

    

    默認構造方法,不對table進行初始化new(真正初始化動作放在put中,後面會看到),只是設置參數的默認值,hashmap長度和table長度初始化成DEFAULT_INITIAL_CAPACITY(16),加載因子loadFactor默認DEFAULT_LOAD_FACTOR(0.75f,至於為什麼是0.75,這個可以參見 )。

    加載因子:默認情況下,16*0.75=12,也就是在存儲第13個元素的時候,就會進行擴容(jdk1.7的threshold真正計算放在第一次初始化中,後面會再提及)。此元素的設置,直接影響到的是key的hash衝突問題。

  3、put方法

 public V put(K key, V value) {
   
if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }

  3.1、EMPTY_TABLE是HashMap中的一個靜態的空的Entry數組,table也是HashMap的一個屬性,默認就是EMPTY_TABLE(這兩句可參見上面源碼),table就是我們真正數據存儲使用的。
  3.2、前面提及,無參構造的時候,並未真正完成對HashMap的初始化new操作,而僅僅只是設置幾個常量,所以在第一次put數據的時候,table是空的。則會進入下面的初始化table方法中。

if (table == EMPTY_TABLE) {
    inflateTable(threshold);
}

private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //計算加載因子,默認情況下結果為12
    table = new Entry[capacity];  //真正的初始化table數組
    initHashSeedAsNeeded(capacity);
}

  3.3、key的null判斷

if (key == null)
    return putForNullKey(value);

private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

  具體步驟解析:

    1、key為null,取出table[0]的鏈表結構Enrty,如果取出的元素不為null,則對其進行循環遍歷,查找其中是否存在key為null的節點元素。

       2、如果存在key == null的節點,則使用新的value去更新節點的oldValue,並且將oldValue返回。

    3、如果不存在key == null的元素,則執行新增元素addEntry方法:

      (1)判斷是否需要擴容,size為當前數組table中,已存放的Entry鏈表個數,更直接點說,就是map.size()方法的返回值。threshold上面的真正初始化HashMap的時候已經提到,默認情況下,計算得到 threshold=12。若同時滿足  (size >= threshold) && (null != table[bucketIndex]) ,則對map進行2倍的擴容,然後對key進行重新計算hash值和新的數組下標。

      (2)創建新的節點原色createEntry方法,首先獲取table數組中下標為bucketIndex的鏈表的表頭元素,然後新建個Entry作為新的表頭,並且新表頭其中的next指向老的表頭數據。

  3.4、key不為null的存儲  
    原理以及過程上通key==null的大體相同,只不過,key==null的時候,固定是獲取table[0]的鏈表進行操作,而在不為key != null的時候,下標位置是通過
  int hash = hash(key); int i = indexFor(hash, table.length); 計算得到的

  static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

  很清晰的就能看明白,先計算key的hash,然後與當前table的長度進行相與,這樣計算得到待存放數據的下標。得到下標后,過程就與key==null一致了,遍歷是否存在,存在則更新並返回oldVlaue,不存在則新建Entry。

  4、get方法

 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
    如果key == null,則調用getForNullKey方法,遍歷table[0]處的鏈表。
private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

  如果key != null,則調用getEntry,根據key計算得到在table數組中的下標,獲取鏈表Entry,然後遍歷查找元素,key相等,則返回該節點元素。

 final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

四、線程不安全分析

  上述,主要淺析了下HashMap的存取過程,HashMap的線程安全性問題主要也就是在上述的擴容resize方法上,下面來看看在高併發下,擴容后,是如何引起100%問題的。

  1、在進行新元素 put 的時候,這在上面中的3.3的代碼片段中可以查看,addEntry 添加新節點的時候,會計算是否需要擴容處理:(size >= threshold) && (null != table[bucketIndex]) 。

  2、如果擴容的話,會接下來調用 resize 方法

 void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        //關鍵性代碼,構建新hashmap並將老的數據移動過來
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

  3、其中,出現100%問題的關鍵就是上面的 transfer 方法,新建hashmap移動複製老數據

 1  void transfer(Entry[] newTable, boolean rehash) {
 2         int newCapacity = newTable.length;
 3         for (Entry<K,V> e : table) {
 4             // 遍歷老的HashMap,當遇到不為空的節點的是,進入移動方法
 5             while(null != e) {
 6                 // 首先創建個Entry節點 指向該節點所在鏈表的下一個節點數據
 7                 Entry<K,V> next = e.next;
 8                 if (rehash) {
 9                     e.hash = null == e.key ? 0 : hash(e.key);
10                 }
11               // 計算老的數據在新Hashmap中的下標位置
12                 int i = indexFor(e.hash, newCapacity);
13              // 將新HashMap中相應位置的元素,掛載到老數據的後面(不管有無數據)
14                 e.next = newTable[i];
15                 // 將新HashMap中相應位置指向上面已經成功掛載新數據的老數據
16              newTable[i] = e;
17              // 移動到鏈表節點中的下一個數據,繼續複製節點
18                 e = next;
19             }
20         }
21     }    

  問題的關鍵就在上述的14、15行上,這兩行的動作,在高併發下可能就會造成循環鏈表,循環鏈表在等待下一個嘗試 get 獲取數據的時候,就悲劇了。下面舉例模擬說說這個過程:

  (1)假設目前某個位置的鏈表存儲結構為 A -> B -> C,有兩個線程同時進行擴容操作

  (2)線程1執行到第7行 Entry<K,V> next = e.next; 的時候被掛起了,此時,線程1的 e 指向 A , next 指向的是 B

  (3)線程2執行完成了整個的擴容過程,那麼此時的鏈表結構應該是變為了 C -> B -> A

  (4)線程1喚醒繼續執行,而需要操作的鏈表實際就變成了了上述線程2完成后的 C ->B -> A,下面分為幾步去完成整個操作:

      第一次循環:

        (i)執行 e.next = newTable[i] ,將 A 的 next 指向線程1的新的HashMap,由於此時無數據,所以 e.next = null

        (ii)執行 newTable[i] = e,將線程1的新的HashMap的第一個元素指向 A 

        (iii)執行e = next,移動到鏈表中的下一個元素,也就是上面的(2)中的 線程掛起的時候的 B

      第二次循環:

        (i)執行 Entry<K,V> next = e.next,此時的 e 指向 B,next指向 A

        (ii)執行 e.next = newTable[i] ,將 B 的 next 指向線程1的新的HashMap,由於此時有數據A,所以 e.next = A

        (iii)執行 newTable[i] = e,將線程1的新的HashMap的第一個元素指向 B,此時線程1的新Hashmap鏈表結構為B -> A

        (iiii)執行e = next,移動到鏈表中的下一個元素 A

      第三次循環:

        (i)執行 Entry<K,V> next = e.next,此時的 e 指向 A,next指向 null

        (ii)執行 e.next = newTable[i] ,將 A 的 next 指向線程1的新的HashMap,由於此時有數據B,所以 e.next = B

        (iii)執行 newTable[i] = e,將線程1的新的HashMap的第一個元素指向 A ,此時線程1的新Hashmap鏈表結構為 A -> B -> A

        (iiii)執行e = next,移動到鏈表中的下一個元素,已移動到鏈表結尾,結束 while 循環,完成鏈表的轉移。

  (5)上述過程中,很顯然的,最終的鏈表結構中,出現了 A -> B -> A 的循環結構。擴容完成了,剩下的等待的是get獲取的時候, getEntry 方法中 for循環e = e.next中就永遠出不來了。

  注意:擴容過程中,newTable是每個擴容線程獨有的,共享的只是每個Entry節點數據,最終的擴容是會調用 table = newTable 賦值操作完成。

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

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

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

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

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

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

5種常見Bean映射工具的性能比對

本文由 JavaGuide 翻譯自 https://www.baeldung.com/java-performance-mapping-frameworks 。轉載請註明原文地址以及翻譯作者。

1. 介紹

創建由多個層組成的大型 Java 應用程序需要使用多種領域模型,如持久化模型、領域模型或者所謂的 DTO。為不同的應用程序層使用多個模型將要求我們提供 bean 之間的映射方法。手動執行此操作可以快速創建大量樣板代碼並消耗大量時間。幸運的是,Java 有多個對象映射框架。在本教程中,我們將比較最流行的 Java 映射框架的性能。

綜合日常使用情況和相關測試數據,個人感覺 MapStruct、ModelMapper 這兩個 Bean 映射框架是最佳選擇。

2. 常見 Bean 映射框架概覽

2.1. Dozer

Dozer 是一個映射框架,它使用遞歸將數據從一個對象複製到另一個對象。框架不僅能夠在 bean 之間複製屬性,還能夠在不同類型之間自動轉換。

要使用 Dozer 框架,我們需要添加這樣的依賴到我們的項目:

<dependency>
    <groupId>net.sf.dozer</groupId>
    <artifactId>dozer</artifactId>
    <version>5.5.1</version>
</dependency>

更多關於 Dozer 的內容可以在官方文檔中找到: http://dozer.sourceforge.net/documentation/gettingstarted.html ,或者你也可以閱讀這篇文章:https://www.baeldung.com/dozer 。

2.2. Orika

Orika 是一個 bean 到 bean 的映射框架,它遞歸地將數據從一個對象複製到另一個對象。

Orika 的工作原理與 Dozer 相似。兩者之間的主要區別是 Orika 使用字節碼生成。這允許以最小的開銷生成更快的映射器。

要使用 Orika 框架,我們需要添加這樣的依賴到我們的項目:

<dependency>
    <groupId>ma.glasnost.orika</groupId>
    <artifactId>orika-core</artifactId>
    <version>1.5.2</version>
</dependency>

更多關於 Orika 的內容可以在官方文檔中找到:https://orika-mapper.github.io/orika-docs/,或者你也可以閱讀這篇文章:https://www.baeldung.com/orika-mapping。

2.3. MapStruct

MapStruct 是一個自動生成 bean mapper 類的代碼生成器。MapStruct 還能夠在不同的數據類型之間進行轉換。Github 地址:https://github.com/mapstruct/mapstruct。

要使用 MapStruct 框架,我們需要添加這樣的依賴到我們的項目:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.2.0.Final</version>
</dependency>

更多關於 MapStruct 的內容可以在官方文檔中找到:https://mapstruct.org/,或者你也可以閱讀這篇文章:https://www.baeldung.com/mapstruct。

要使用 MapStruct 框架,我們需要添加這樣的依賴到我們的項目:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.2.0.Final</version>
</dependency>

2.4. ModelMapper

ModelMapper 是一個旨在簡化對象映射的框架,它根據約定確定對象之間的映射方式。它提供了類型安全的和重構安全的 API。

更多關於 ModelMapper 的內容可以在官方文檔中找到:http://modelmapper.org/ 。

要使用 ModelMapper 框架,我們需要添加這樣的依賴到我們的項目:

<dependency>
  <groupId>org.modelmapper</groupId>
  <artifactId>modelmapper</artifactId>
  <version>1.1.0</version>
</dependency>

2.5. JMapper

JMapper 是一個映射框架,旨在提供易於使用的、高性能的 Java bean 之間的映射。該框架旨在使用註釋和關係映射應用 DRY 原則。該框架允許不同的配置方式:基於註釋、XML 或基於 api。

更多關於 JMapper 的內容可以在官方文檔中找到:https://github.com/jmapper-framework/jmapper-core/wiki。

要使用 JMapper 框架,我們需要添加這樣的依賴到我們的項目:

<dependency>
    <groupId>com.googlecode.jmapper-framework</groupId>
    <artifactId>jmapper-core</artifactId>
    <version>1.6.0.1</version>
</dependency>

3.測試模型

為了能夠正確地測試映射,我們需要有一個源和目標模型。我們已經創建了兩個測試模型。

第一個是一個只有一個字符串字段的簡單 POJO,它允許我們在更簡單的情況下比較框架,並檢查如果我們使用更複雜的 bean 是否會發生任何變化。

簡單的源模型如下:

public class SourceCode {
    String code;
    // getter and setter
}

它的目標也很相似:

public class DestinationCode {
    String code;
    // getter and setter
}

源 bean 的實際示例如下:

public class SourceOrder {
    private String orderFinishDate;
    private PaymentType paymentType;
    private Discount discount;
    private DeliveryData deliveryData;
    private User orderingUser;
    private List<Product> orderedProducts;
    private Shop offeringShop;
    private int orderId;
    private OrderStatus status;
    private LocalDate orderDate;
    // standard getters and setters
}

目標類如下圖所示:

public class Order {
    private User orderingUser;
    private List<Product> orderedProducts;
    private OrderStatus orderStatus;
    private LocalDate orderDate;
    private LocalDate orderFinishDate;
    private PaymentType paymentType;
    private Discount discount;
    private int shopId;
    private DeliveryData deliveryData;
    private Shop offeringShop;
    // standard getters and setters
}

整個模型結構可以在這裏找到:https://github.com/eugenp/tutorials/tree/master/performance-tests/src/main/java/com/baeldung/performancetests/model/source。

4. 轉換器

為了簡化測試設置的設計,我們創建了如下所示的轉換器接口:

public interface Converter {
    Order convert(SourceOrder sourceOrder);
    DestinationCode convert(SourceCode sourceCode);
}

我們所有的自定義映射器都將實現這個接口。

4.1. OrikaConverter

Orika 支持完整的 API 實現,這大大簡化了 mapper 的創建:

public class OrikaConverter implements Converter{
    private MapperFacade mapperFacade;

    public OrikaConverter() {
        MapperFactory mapperFactory = new DefaultMapperFactory
          .Builder().build();

        mapperFactory.classMap(Order.class, SourceOrder.class)
          .field("orderStatus", "status").byDefault().register();
        mapperFacade = mapperFactory.getMapperFacade();
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
        return mapperFacade.map(sourceOrder, Order.class);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return mapperFacade.map(sourceCode, DestinationCode.class);
    }
}

4.2. DozerConverter

Dozer 需要 XML 映射文件,有以下幾個部分:

<mappings xmlns="http://dozer.sourceforge.net"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://dozer.sourceforge.net
  http://dozer.sourceforge.net/schema/beanmapping.xsd">

    <mapping>
        <class-a>com.baeldung.performancetests.model.source.SourceOrder</class-a>
        <class-b>com.baeldung.performancetests.model.destination.Order</class-b>
        <field>
            <a>status</a>
            <b>orderStatus</b>
        </field>
    </mapping>
    <mapping>
        <class-a>com.baeldung.performancetests.model.source.SourceCode</class-a>
        <class-b>com.baeldung.performancetests.model.destination.DestinationCode</class-b>
    </mapping>
</mappings>

定義了 XML 映射后,我們可以從代碼中使用它:

public class DozerConverter implements Converter {
    private final Mapper mapper;

    public DozerConverter() {
        DozerBeanMapper mapper = new DozerBeanMapper();
        mapper.addMapping(
          DozerConverter.class.getResourceAsStream("/dozer-mapping.xml"));
        this.mapper = mapper;
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
        return mapper.map(sourceOrder,Order.class);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return mapper.map(sourceCode, DestinationCode.class);
    }
}

4.3. MapStructConverter

Map 結構的定義非常簡單,因為它完全基於代碼生成:

@Mapper
public interface MapStructConverter extends Converter {
    MapStructConverter MAPPER = Mappers.getMapper(MapStructConverter.class);

    @Mapping(source = "status", target = "orderStatus")
    @Override
    Order convert(SourceOrder sourceOrder);

    @Override
    DestinationCode convert(SourceCode sourceCode);
}

4.4. JMapperConverter

JMapperConverter 需要做更多的工作。接口實現后:

public class JMapperConverter implements Converter {
    JMapper realLifeMapper;
    JMapper simpleMapper;

    public JMapperConverter() {
        JMapperAPI api = new JMapperAPI()
          .add(JMapperAPI.mappedClass(Order.class));
        realLifeMapper = new JMapper(Order.class, SourceOrder.class, api);
        JMapperAPI simpleApi = new JMapperAPI()
          .add(JMapperAPI.mappedClass(DestinationCode.class));
        simpleMapper = new JMapper(
          DestinationCode.class, SourceCode.class, simpleApi);
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
        return (Order) realLifeMapper.getDestination(sourceOrder);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return (DestinationCode) simpleMapper.getDestination(sourceCode);
    }
}

我們還需要向目標類的每個字段添加@JMap註釋。此外,JMapper 不能在 enum 類型之間轉換,它需要我們創建自定義映射函數:

@JMapConversion(from = "paymentType", to = "paymentType")
public PaymentType conversion(com.baeldung.performancetests.model.source.PaymentType type) {
    PaymentType paymentType = null;
    switch(type) {
        case CARD:
            paymentType = PaymentType.CARD;
            break;

        case CASH:
            paymentType = PaymentType.CASH;
            break;

        case TRANSFER:
            paymentType = PaymentType.TRANSFER;
            break;
    }
    return paymentType;
}

4.5. ModelMapperConverter

ModelMapperConverter 只需要提供我們想要映射的類:

public class ModelMapperConverter implements Converter {
    private ModelMapper modelMapper;

    public ModelMapperConverter() {
        modelMapper = new ModelMapper();
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
       return modelMapper.map(sourceOrder, Order.class);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return modelMapper.map(sourceCode, DestinationCode.class);
    }
}

5. 簡單的模型測試

對於性能測試,我們可以使用 Java Microbenchmark Harness,關於如何使用它的更多信息可以在 這篇文章:https://www.baeldung.com/java-microbenchmark-harness 中找到。

我們為每個轉換器創建了一個單獨的基準測試,並將基準測試模式指定為 Mode.All。

5.1. 平均時間

對於平均運行時間,JMH 返回以下結果(越少越好):

這個基準測試清楚地表明,MapStruct 和 JMapper 都有最佳的平均工作時間。

5.2. 吞吐量

在這種模式下,基準測試返回每秒的操作數。我們收到以下結果(越多越好):

在吞吐量模式中,MapStruct 是測試框架中最快的,JMapper 緊隨其後。

5.3. SingleShotTime

這種模式允許測量單個操作從開始到結束的時間。基準給出了以下結果(越少越好):

這裏,我們看到 JMapper 返回的結果比 MapStruct 好得多。

5.4. 採樣時間

這種模式允許對每個操作的時間進行採樣。三個不同百分位數的結果如下:

所有的基準測試都表明,根據場景的不同,MapStruct 和 JMapper 都是不錯的選擇,儘管 MapStruct 對 SingleShotTime 給出的結果要差得多。

6. 真實模型測試

對於性能測試,我們可以使用 Java Microbenchmark Harness,關於如何使用它的更多信息可以在 這篇文章:https://www.baeldung.com/java-microbenchmark-harness 中找到。

我們為每個轉換器創建了一個單獨的基準測試,並將基準測試模式指定為 Mode.All。

6.1. 平均時間

JMH 返回以下平均運行時間結果(越少越好):

該基準清楚地表明,MapStruct 和 JMapper 均具有最佳的平均工作時間。

6.2. 吞吐量

在這種模式下,基準測試返回每秒的操作數。我們收到以下結果(越多越好):

在吞吐量模式中,MapStruct 是測試框架中最快的,JMapper 緊隨其後。

6.3. SingleShotTime

這種模式允許測量單個操作從開始到結束的時間。基準給出了以下結果(越少越好):

6.4. 採樣時間

這種模式允許對每個操作的時間進行採樣。三個不同百分位數的結果如下:

儘管簡單示例和實際示例的確切結果明顯不同,但是它們的趨勢相同。在哪種算法最快和哪種算法最慢方面,兩個示例都給出了相似的結果。

6.5. 結論

根據我們在本節中執行的真實模型測試,我們可以看出,最佳性能顯然屬於 MapStruct。在相同的測試中,我們看到 Dozer 始終位於結果表的底部。

7. 總結

在這篇文章中,我們已經進行了五個流行的 Java Bean 映射框架性能測試:ModelMapper MapStruct Orika ,Dozer, JMapper。

示例代碼地址:https://github.com/eugenp/tutorials/tree/master/performance-tests。

開源項目推薦

作者的其他開源項目推薦:

  1. :【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。
  2. : 適合新手入門以及有經驗的開發人員查閱的 Spring Boot 教程(業餘時間維護中,歡迎一起維護)。
  3. : 我覺得技術人員應該有的一些好習慣!
  4. :從零入門 !Spring Security With JWT(含權限驗證)後端部分代碼。

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

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

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

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

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

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

EFK教程 – ElasticSearch高性能高可用架構

通過將elasticsearch的data、ingest、master角色進行分離,搭建起高性能+高可用的ES架構

作者:“發顛的小狼”,歡迎轉載與投稿

目錄

▪ 用途
▪ 架構
▪ 步驟說明
▪ elasticsearch-data部署
▪ elasticsearch-ingest部署
▪ elasticsearch-master部署

用途

在第一篇《EFK教程 – 快速入門指南》中,闡述了EFK的安裝部署,其中ES的架構為三節點,即master、ingest、data角色同時部署在三台服務器上。

在本文中,將進行角色分離部署,並且每個角色分別部署三節點,在實現性能最大化的同時保障高可用。

▷ elasticsearch的master節點:用於調度,採用普通性能服務器來部署
▷ elasticsearch的ingest節點:用於數據預處理,採用性能好的服務器來部署
▷ elasticsearch的data節點:用於數據落地存儲,採用存儲性能好的服務器來部署

若不知道去哪找《EFK教程 - 快速入門指南》,可在主流搜索引擎里搜索:
小慢哥 EFK教程 快速入門指南
或者
小慢哥 EFK教程 基於多節點ES的EFK安裝部署配置

架構

服務器配置

注意:此處的架構是之前的文章《EFK教程 – 快速入門指南》的拓展,因此請先按照《EFK教程 – 快速入門指南》完成部署

步驟說明

1️⃣ 部署3台data節點,加入原集群
2️⃣ 部署3台ingest節點,加入原集群
3️⃣ 將原有的es索引遷移到data節點
4️⃣ 將原有的es節點改造成master節點

elasticsearch-data部署

之前已完成了基礎的elasticsearch架構,現需要新增三台存儲節點加入集群,同時關閉master和ingest功能

elasticsearch-data安裝:3台均執行相同的安裝步驟

tar -zxvf elasticsearch-7.3.2-linux-x86_64.tar.gz
mv elasticsearch-7.3.2 /opt/elasticsearch
useradd elasticsearch -d /opt/elasticsearch -s /sbin/nologin
mkdir -p /opt/logs/elasticsearch
chown elasticsearch.elasticsearch /opt/elasticsearch -R
chown elasticsearch.elasticsearch /opt/logs/elasticsearch -R
# 數據盤需要elasticsearch寫權限
chown elasticsearch.elasticsearch /data/SAS -R

# 限制一個進程可以擁有的VMA(虛擬內存區域)的數量要超過262144,不然elasticsearch會報max virtual memory areas vm.max_map_count [65535] is too low, increase to at least [262144]
echo "vm.max_map_count = 655350" >> /etc/sysctl.conf
sysctl -p

elasticsearch-data配置

▷ 192.168.1.51 /opt/elasticsearch/config/elasticsearch.yml

cluster.name: my-application
node.name: 192.168.1.51
# 數據盤位置,如果有多個硬盤位置,用","隔開
path.data: /data/SAS
path.logs: /opt/logs/elasticsearch
network.host: 192.168.1.51

discovery.seed_hosts: ["192.168.1.31","192.168.1.32","192.168.1.33"]
cluster.initial_master_nodes: ["192.168.1.31","192.168.1.32","192.168.1.33"]
http.cors.enabled: true
http.cors.allow-origin: "*"

# 關閉master功能
node.master: false
# 關閉ingest功能
node.ingest: false
# 開啟data功能
node.data: true

▷ 192.168.1.52 /opt/elasticsearch/config/elasticsearch.yml

cluster.name: my-application
node.name: 192.168.1.52
# 數據盤位置,如果有多個硬盤位置,用","隔開
path.data: /data/SAS
path.logs: /opt/logs/elasticsearch
network.host: 192.168.1.52

discovery.seed_hosts: ["192.168.1.31","192.168.1.32","192.168.1.33"]
cluster.initial_master_nodes: ["192.168.1.31","192.168.1.32","192.168.1.33"]
http.cors.enabled: true
http.cors.allow-origin: "*"

# 關閉master功能
node.master: false
# 關閉ingest功能
node.ingest: false
# 開啟data功能
node.data: true

▷ 192.168.1.53 /opt/elasticsearch/config/elasticsearch.yml

cluster.name: my-application
node.name: 192.168.1.53
# 數據盤位置,如果有多個硬盤位置,用","隔開
path.data: /data/SAS
path.logs: /opt/logs/elasticsearch
network.host: 192.168.1.53

discovery.seed_hosts: ["192.168.1.31","192.168.1.32","192.168.1.33"]
cluster.initial_master_nodes: ["192.168.1.31","192.168.1.32","192.168.1.33"]
http.cors.enabled: true
http.cors.allow-origin: "*"

# 關閉master功能
node.master: false
# 關閉ingest功能
node.ingest: false
# 開啟data功能
node.data: true

elasticsearch-data啟動

sudo -u elasticsearch /opt/elasticsearch/bin/elasticsearch

elasticsearch集群狀態

curl "http://192.168.1.31:9200/_cat/health?v"

elasticsearch-data狀態

curl "http://192.168.1.31:9200/_cat/nodes?v"

elasticsearch-data參數說明

status: green  # 集群健康狀態
node.total: 6  # 有6台機子組成集群
node.data: 6  # 有6個節點的存儲
node.role: d  # 只擁有data角色
node.role: i  # 只擁有ingest角色
node.role: m  # 只擁有master角色
node.role: mid  # 擁master、ingest、data角色

elasticsearch-ingest部署

現需要新增三台ingest節點加入集群,同時關閉master和data功能

elasticsearch-ingest安裝:3台es均執行相同的安裝步驟

tar -zxvf elasticsearch-7.3.2-linux-x86_64.tar.gz
mv elasticsearch-7.3.2 /opt/elasticsearch
useradd elasticsearch -d /opt/elasticsearch -s /sbin/nologin
mkdir -p /opt/logs/elasticsearch
chown elasticsearch.elasticsearch /opt/elasticsearch -R
chown elasticsearch.elasticsearch /opt/logs/elasticsearch -R

# 限制一個進程可以擁有的VMA(虛擬內存區域)的數量要超過262144,不然elasticsearch會報max virtual memory areas vm.max_map_count [65535] is too low, increase to at least [262144]
echo "vm.max_map_count = 655350" >> /etc/sysctl.conf
sysctl -p

elasticsearch-ingest配置

▷ 192.168.1.41 /opt/elasticsearch/config/elasticsearch.yml

cluster.name: my-application
node.name: 192.168.1.41
path.logs: /opt/logs/elasticsearch
network.host: 192.168.1.41

discovery.seed_hosts: ["192.168.1.31","192.168.1.32","192.168.1.33"]
cluster.initial_master_nodes: ["192.168.1.31","192.168.1.32","192.168.1.33"]
http.cors.enabled: true
http.cors.allow-origin: "*"

# 關閉master功能
node.master: false
# 開啟ingest功能
node.ingest: true
# 關閉data功能
node.data: false

▷ 192.168.1.42 /opt/elasticsearch/config/elasticsearch.yml

cluster.name: my-application
node.name: 192.168.1.42
path.logs: /opt/logs/elasticsearch
network.host: 192.168.1.42

discovery.seed_hosts: ["192.168.1.31","192.168.1.32","192.168.1.33"]
cluster.initial_master_nodes: ["192.168.1.31","192.168.1.32","192.168.1.33"]
http.cors.enabled: true
http.cors.allow-origin: "*"

# 關閉master功能
node.master: false
# 開啟ingest功能
node.ingest: true
# 關閉data功能
node.data: false

▷ 192.168.1.43 /opt/elasticsearch/config/elasticsearch.yml

cluster.name: my-application
node.name: 192.168.1.43
path.logs: /opt/logs/elasticsearch
network.host: 192.168.1.43

discovery.seed_hosts: ["192.168.1.31","192.168.1.32","192.168.1.33"]
cluster.initial_master_nodes: ["192.168.1.31","192.168.1.32","192.168.1.33"]
http.cors.enabled: true
http.cors.allow-origin: "*"

# 關閉master功能
node.master: false
# 開啟ingest功能
node.ingest: true
# 關閉data功能
node.data: false

elasticsearch-ingest啟動

sudo -u elasticsearch /opt/elasticsearch/bin/elasticsearch

elasticsearch集群狀態

curl "http://192.168.1.31:9200/_cat/health?v"

elasticsearch-ingest狀態

curl "http://192.168.1.31:9200/_cat/nodes?v"

elasticsearch-ingest參數說明

status: green  # 集群健康狀態
node.total: 9  # 有9台機子組成集群
node.data: 6  # 有6個節點的存儲
node.role: d  # 只擁有data角色
node.role: i  # 只擁有ingest角色
node.role: m  # 只擁有master角色
node.role: mid  # 擁master、ingest、data角色

elasticsearch-master部署

首先,將上一篇《EFK教程 – 快速入門指南》中部署的3台es(192.168.1.31、192.168.1.32、192.168.1.33)改成只有master的功能, 因此需要先將這3台上的索引數據遷移到本次所做的data節點中

1️⃣ 索引遷移:一定要做這步,將之前的索引放到data節點上

curl -X PUT "192.168.1.31:9200/*/_settings?pretty" -H 'Content-Type: application/json' -d'
{
  "index.routing.allocation.include._ip": "192.168.1.51,192.168.1.52,192.168.1.53"
}'

2️⃣ 確認當前索引存儲位置:確認所有索引不在192.168.1.31、192.168.1.32、192.168.1.33節點上

curl "http://192.168.1.31:9200/_cat/shards?h=n"

elasticsearch-master配置

注意事項:修改配置,重啟進程,需要一台一台執行,要確保第一台成功后,再執行下一台。重啟進程的方法:由於上一篇文章《EFK教程 – 快速入門指南》里,是執行命令跑在前台,因此直接ctrl – c退出再啟動即可,啟動命令如下

sudo -u elasticsearch /opt/elasticsearch/bin/elasticsearch

▷ 192.168.1.31 /opt/elasticsearch/config/elasticsearch.yml

cluster.name: my-application
node.name: 192.168.1.31
path.logs: /opt/logs/elasticsearch
network.host: 192.168.1.31

discovery.seed_hosts: ["192.168.1.31","192.168.1.32","192.168.1.33"]
cluster.initial_master_nodes: ["192.168.1.31","192.168.1.32","192.168.1.33"]
http.cors.enabled: true
http.cors.allow-origin: "*"

#開啟master功能
node.master: true
#關閉ingest功能
node.ingest: false
#關閉data功能
node.data: false

▷ 192.168.1.32 /opt/elasticsearch/config/elasticsearch.yml

cluster.name: my-application
node.name: 192.168.1.32
path.logs: /opt/logs/elasticsearch
network.host: 192.168.1.32

discovery.seed_hosts: ["192.168.1.31","192.168.1.32","192.168.1.33"]
cluster.initial_master_nodes: ["192.168.1.31","192.168.1.32","192.168.1.33"]
http.cors.enabled: true
http.cors.allow-origin: "*"

#開啟master功能
node.master: true
#關閉ingest功能
node.ingest: false
#關閉data功能
node.data: false

▷ 192.168.1.33 /opt/elasticsearch/config/elasticsearch.yml

cluster.name: my-application
node.name: 192.168.1.33
path.logs: /opt/logs/elasticsearch
network.host: 192.168.1.33

discovery.seed_hosts: ["192.168.1.31","192.168.1.32","192.168.1.33"]
cluster.initial_master_nodes: ["192.168.1.31","192.168.1.32","192.168.1.33"]
http.cors.enabled: true
http.cors.allow-origin: "*"

#開啟master功能
node.master: true
#關閉ingest功能
node.ingest: false
#關閉data功能
node.data: false

elasticsearch集群狀態

curl "http://192.168.1.31:9200/_cat/health?v"

elasticsearch-master狀態

curl "http://192.168.1.31:9200/_cat/nodes?v"

至此,當node.role里所有服務器都不再出現“mid”,則表示一切順利完成。

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

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

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

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

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

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

科學家發現新型態鈾 恐影響核廢料處理計畫

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

擴大中國電動車佈局 鴻海今年據點數可能突破 10 個

鴻海集團擬擴大在大陸電動車租車佈局,繼成功搶佔北京、杭州、常州後,7 月即將前進貴陽。鴻海指出,去年北京成功試點後,已累積相當經驗,今年將加速複製,內部規畫今年據點數可望突破 10 個,相較去年僅有北京 1 個據點,年成長逾 10 倍。   鴻海進軍電動車市場動作頻頻,繼去年與大陸北汽集團合作,共同組成新能源汽車租賃公司北京恆譽,搶進分時租車市場後,歷經一年,目前北京恆譽共設有 200 個充電椿,並提供 1,000 多輛北汽「E150EV」電動車供租賃。   今年鴻海集團還將大刀闊斧展開擴點,光是上半年就已順利進駐杭州、常州,貴陽也預定 7 月進駐,屆時將成立黔譽的子公司負責管理。據了解,目前鴻海手中還有幾個新點在考慮中,包括上海、深圳、梧州,目標是今年底前總據點數突破 10 個的基本門檻。   法人指出,鴻海透過電動車租賃,提前卡位電動車市場,目標不單是電動車租賃市場,更重要的是後面的代工市場,將電動車市場大餅做大,最大獲益者還是鴻海集團。

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

【其他文章推薦】

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

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

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

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

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

MySQL 執行計劃詳解

MySQL 原理篇

我們經常使用 MySQL 的執行計劃來查看 SQL 語句的執行效率,接下來分析執行計劃的各個显示內容。

EXPLAIN SELECT * FROM users 
WHERE id IN (SELECT userID FROMuser_address WHERE address = "湖南長沙麓谷") ;

執行計劃的 id

select 查詢的序列號,標識執行的順序

  • id 相同,執行順序由上至下
  • id 不同,如果是子查詢,id 的序號會遞增,id 值越大優先級越高,越先被執行

執行計劃的 select_type

查詢的類型,主要是用於區分普通查詢、聯合查詢、子查詢等。

  • SIMPLE:簡單的 select 查詢,查詢中不包含子查詢或者 union
  • PRIMARY:查詢中包含子部分,最外層查詢則被標記為 primary
  • SUBQUERY/MATERIALIZED:SUBQUERY 表示在 select 或 where 列表中包含了子查詢,MATERIALIZED表示 where 後面 in 條件的子查詢
  • UNION:表示 union 中的第二個或後面的 select 語句
  • UNION RESULT:union 的結果

對於 UNION 和 UNION RESULT 可以通過下面的例子展現:

EXPLAIN
SELECT * FROM users WHERE id IN(1, 2)
UNION
SELECT * FROM users WHERE id IN(3, 4);

執行計劃的 table

查詢涉及到的表。

  • 直接顯示錶名或者表的別名
  • <unionM,N> 由 ID 為 M,N 查詢 union 產生的結果
  • <subqueryN> 由 ID 為 N 查詢產生的結果

執行計劃的 type 

訪問類型,SQL 查詢優化中一個很重要的指標,結果值從好到壞依次是:system > const > eq_ref > ref > range > index > ALL。

  • system:系統表,少量數據,往往不需要進行磁盤IO
  • const:常量連接
  • eq_ref:主鍵索引(primary key)或者非空唯一索引(unique not null)等值掃描
  • ref:非主鍵非唯一索引等值掃描
  • range:範圍掃描
  • index:索引樹掃描
  • ALL:全表掃描(full table scan)

下面通過舉例說明。

system

explain select * from mysql.time_zone;

上例中,從系統庫 MySQL 的系統表 time_zone 里查詢數據,訪問類型為 system,這些數據已經加載到內存里,不需要進行磁盤 IO,這類掃描是速度最快的。

explain select * from (select * from user where id=1) tmp;

再舉一個例子,內層嵌套(const)返回了一個臨時表,外層嵌套從臨時表查詢,其掃描類型也是 system,也不需要走磁盤 IO,速度超快。

const 

數據準備:

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `NAME` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user values(1,'shenjian');
insert into user values(2,'zhangsan');
insert into user values(3,'lisi');
explain select * from user where id=1;

const 掃描的條件為: 

  1. 命中主鍵(primary key)或者唯一(unique)索引
  2. 被連接的部分是一個常量(const)值 

如上例,id 是 主鍵索引,連接部分是常量1。

eq_ref

數據準備:

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `NAME` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user values(1,'shenjian');
insert into user values(2,'zhangsan');
insert into user values(3,'lisi');

CREATE TABLE `user_ex` (
  `id` int(11) NOT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user_ex values(1,18);
insert into user_ex values(2,20);
insert into user_ex values(3,30);
insert into user_ex values(4,40);
insert into user_ex values(5,50);
EXPLAIN SELECT * FROM USER,user_ex WHERE user.id=user_ex.id;

eq_ref 掃描的條件為,對於前表的每一行(row),后表只有一行被掃描。 

再細化一點:  

  1. join 查詢
  2. 命中主鍵(primary key)或者非空唯一(unique not null)索引
  3. 等值連接;

如上例,id 是主鍵,該 join 查詢為 eq_ref 掃描。

ref

數據準備:

CREATE TABLE `user` (
  `id` int(11) DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL,
  KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user values(1,'shenjian');
insert into user values(2,'zhangsan');
insert into user values(3,'lisi');

CREATE TABLE `user_ex` (
  `id` int(11) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user_ex values(1,18);
insert into user_ex values(2,20);
insert into user_ex values(3,30);
insert into user_ex values(4,40);
insert into user_ex values(5,50);
EXPLAIN SELECT * FROM USER,user_ex WHERE user.id=user_ex.id;

如果把上例 eq_ref 案例中的主鍵索引,改為普通非唯一(non unique)索引。就由 eq_ref 降級為了 ref,此時對於前表的每一行(row),后表可能有多於一行的數據被掃描。

select * from user where id=1;

當 id 改為普通非唯一索引后,常量的連接查詢,也由 const 降級為了 ref,因為也可能有多於一行的數據被掃描。

ref 掃描,可能出現在 join 里,也可能出現在單表普通索引里,每一次匹配可能有多行數據返回,雖然它比 eq_ref 要慢,但它仍然是一個很快的 join 類型。

range

數據準備:

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user values(1,'shenjian');
insert into user values(2,'zhangsan');
insert into user values(3,'lisi');
insert into user values(4,'wangwu');
insert into user values(5,'zhaoliu');
explain select * from user where id between 1 and 4;
explain select * from user where id in(1,2,3);
explain select * from user where id > 3;

range 掃描就比較好理解了,它是索引上的範圍查詢,它會在索引上掃碼特定範圍內的值。

像上例中的 between,in,> 都是典型的範圍(range)查詢。

index

explain count (*) from user;

如上例,id 是主鍵,該 count 查詢需要通過掃描索引上的全部數據來計數,它僅比全表掃描快一點。

ALL 

數據準備:

CREATE TABLE `user` (
  `id` int(11) DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user values(1,'shenjian');
insert into user values(2,'zhangsan');
insert into user values(3,'lisi');

CREATE TABLE `user_ex` (
  `id` int(11) DEFAULT NULL,
  `age` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user_ex values(1,18);
insert into user_ex values(2,20);
insert into user_ex values(3,30);
insert into user_ex values(4,40);
insert into user_ex values(5,50);
explain select * from user,user_ex where user.id=user_ex.id;

如果 id 上不建索引,對於前表的每一行(row),后表都要被全表掃描。

文章中,這個相同的 join 語句出現了三次:

  1. 掃描類型為 eq_ref,此時 id 為主鍵
  2. 掃描類型為 ref,此時 id 為非唯一普通索引
  3. 掃描類型為 ALL,全表掃描,此時id上無索引

有此可見,建立正確的索引,對數據庫性能的提升是多麼重要。

總結 

  1. explain 結果中的 type 字段,表示(廣義)連接類型,它描述了找到所需數據使用的掃描方式;
  2. 常見的掃描類型有:system>const>eq_ref>ref>range>index>ALL,其掃描速度由快到慢;
  3. 各類掃描類型的要點是:
    1. system 最快:不進行磁盤 IO
    2. const:PK 或者 unique 上的等值查詢
    3. eq_ref:PK 或者 unique 上的 join 查詢,等值匹配,對於前表的每一行,后表只有一行命中
    4. ref:非唯一索引,等值匹配,可能有多行命中
    5. range:索引上的範圍掃描,例如:between、in、>
    6. index:索引上的全集掃描,例如:InnoDB 的 count
    7. ALL 最慢:全表掃描
  1. 建立正確的索引,非常重要;
  2. 使用 explain 了解並優化執行計劃,非常重要;

執行計劃 possible_keys

查詢過程中有可能用到的索引。

執行計劃 key

實際使用的索引,如果為 NULL ,則沒有使用索引。

執行計劃 rows

根據表統計信息或者索引選用情況,大致估算出找到所需的記錄所需要讀取的行數。

執行計劃 filtered 

表示返回結果的行數占需讀取行數的百分比, filtered 的值越大越好。

執行計劃 Extra 

十分重要的額外信息。

  • Using filesort:MySQL 對數據使用一個外部的文件內容進行了排序,而不是按照表內的索引進行排序讀取。
  • Using temporary:使用臨時表保存中間結果,也就是說 MySQL 在對查詢結果排序時使用了臨時表,常見於order by 或 group by。
  • Using index:表示 SQL 操作中使用了覆蓋索引(Covering Index),避免了訪問表的數據行,效率高。
  • Using index condition:表示 SQL 操作命中了索引,但不是所有的列數據都在索引樹上,還需要訪問實際的行記錄。
  • Using where:表示 SQL 操作使用了 where 過濾條件。
  • Select tables optimized away:基於索引優化 MIN/MAX 操作或者 MyISAM 存儲引擎優化 COUNT(*) 操作,不必等到執行階段再進行計算,查詢執行計劃生成的階段即可完成優化。
  • Using join buffer (Block Nested Loop):表示 SQL 操作使用了關聯查詢或者子查詢,且需要進行嵌套循環計算。

下面通過舉例說明。

數據準備:

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  `sex` varchar(5) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

insert into user values(1, 'shenjian','no');
insert into user values(2, 'zhangsan','no');
insert into user values(3, 'lisi', 'yes');
insert into user values(4, 'lisi', 'no');

數據說明:

用戶表:id 主鍵索引,name 普通索引(非唯一),sex 無索引。

四行記錄:其中 name 普通索引存在重複記錄 lisi。

Using filesort

explain select * from user order by sex;

 

Extra 為 Using filesort 說明,得到所需結果集,需要對所有記錄進行文件排序。

這類 SQL 語句性能極差,需要進行優化。

典型的,在一個沒有建立索引的列上進行了 order by,就會觸發 filesort,常見的優化方案是,在 order by 的列上添加索引,避免每次查詢都全量排序。

Using temporary

explain select * from user group by name order by sex;

Extra 為 Using temporary 說明,需要建立臨時表(temporary table)來暫存中間結果。

這類 SQL 語句性能較低,往往也需要進行優化。

典型的 group by 和 order by 同時存在,且作用於不同的字段時,就會建立臨時表,以便計算出最終的結果集。

臨時表存在兩種引擎,一種是 Memory 引擎,一種是 MyISAM 引擎,如果返回的數據在 16M 以內(默認),且沒有大字段的情況下,使用 Memory 引擎,否則使用 MyISAM 引擎。 

Using index

EXPLAIN SELECT id FROM USER;

Extra 為 Using index 說明,SQL 所需要返回的所有列數據均在一棵索引樹上,而無需訪問實際的行記錄。

這類 SQL 語句往往性能較好。

Using index condition

explain select id, name, sex from user where name='shenjian';

Extra 為 Using index condition 說明,確實命中了索引,但不是所有的列數據都在索引樹上,還需要訪問實際的行記錄。

這類 SQL 語句性能也較高,但不如 Using index。

Using where

explain select * from user where sex='no';

Extra 為 Using where 說明,查詢的結果集使用了 where 過濾條件,比如上面的 SQL 使用了 sex = 'no' 的過濾條件

Select tables optimized away

EXPLAIN SELECT MAX(id) FROM USER;

 

比如上面的語句查詢 id 的最大值,因為 id 是主鍵索引,根據 B+Tree 的結構,天然就是有序存放的,所以不需要等到執行階段再進行計算,查詢執行計劃生成的階段即可完成優化。

Using join buffer (Block Nested Loop)

explain select * from user where id in (select id from user where sex='no');

Extra 為 Using join buffer (Block Nested Loop) 說明,需要進行嵌套循環計算。內層和外層的 type 均為 ALL,rows 均為4,需要循環進行4*4次計算。

這類 SQL 語句性能往往也較低,需要進行優化。

典型的兩個關聯表 join,關聯字段均未建立索引,就會出現這種情況。常見的優化方案是,在關聯字段上添加索引,避免每次嵌套循環計算。

參考 

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

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

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

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

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

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

北京解讀純電動客車補貼政策

今天(10日)上午,北京市對購買純電動客車有關財政政策進行解讀。北京提供1∶1市級財政補貼,兩級財政補貼總額最高不超過車輛銷售價格的60%。  
補貼範圍   行政事業單位使用財政資金購買純電動客車不納入通知規定的補助範圍。地方純電動客車補貼,直接撥付到汽車生產企業,消費者按銷售價格扣減補助後支付。  
按車長補貼   2015年純電動客車推廣應用補助標準按車長執行,車長超過10米的車補助標準最高,達50萬元 2016年補助標準中引入車長在10米-12米之間的“標準車”概念,在單位載品質能量消耗量小於0.25、續駛里程超過250公里時,補貼金額最多,可達50萬 12米以上、雙層客車參照標準車1.2倍給予補助 8-10米客車,按照標準車0.8倍給予補助 6-8米客車,參照標準車的0.5倍給予補貼 6米及以下客車參照標準車的0.2倍給予補助 2017年補助標準在2016年基礎上下調20%   面向全國開放純電動客車市場,北京市採用備案制管理,只要納入國家《節能與新能源汽車示範推廣應用工程推薦車型目錄》和《免征車輛購置稅的新能源汽車車型目錄》,完成企業和產品備案,即可在京銷售。   凡是滿足交通行業相關規定的運營企業均可購置、運營純電動客車。鼓勵公交、公路客運、旅遊等領域的企業積極開展純電動客車示範推廣工作;積極鼓勵開展各種純電動客車車型示範應用。

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

【其他文章推薦】

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

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

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

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

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

mpvue+小程序雲開發,純前端實現婚禮邀請函(相冊小程序)

請勿使用本文章及源碼作為商業用途!

前言

當初做這個小程序是為了婚禮前的需要,結婚之後,希望這個小程序能夠留存下來,特地花了一些空閑時間將小程序轉化成為“相冊類小程序”

體驗碼

準備工作

  1. mpvue框架 
  2. 小程序·雲開發 

注意:使用mpvue前,首先你得先熟悉vue框架的基本使用 

項目結構介紹

注意:接下來展示的代碼,有幾個對比,分別是本人優化前和優化后的代碼對比,感興趣的可以着重看一下優化后的成熟寫法。

  • common目錄: 放一些公共資源,如js,css,json
  • components目錄:組件相關的.vue文件都放在這裏
  • pages目錄:所有頁面都放在這個目錄
  • utils目錄:使用mpvue時自動生成,可忽略
  • app.json文件:
{
  "pages": [ "pages/index/main", "pages/photo/main", "pages/map/main", "pages/greet/main", "pages/message/main" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle": "black" }, "tabBar": { "color": "#ccc", "selectedColor": "#ff4c91", "borderStyle": "white", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/index/main", "iconPath": "static/images/1-1.png", "selectedIconPath": "static/images/1-2.png", "text": "邀請函" }, { "pagePath": "pages/photo/main", "iconPath": "static/images/2-1.png", "selectedIconPath": "static/images/2-2.png", "text": "甜蜜相冊" }, { "pagePath": "pages/map/main", "iconPath": "static/images/3-1.png", "selectedIconPath": "static/images/3-2.png", "text": "酒店導航" }, { "pagePath": "pages/greet/main", "iconPath": "static/images/4-1.png", "selectedIconPath": "static/images/4-2.png", "text": "好友祝福" }, { "pagePath": "pages/message/main", "iconPath": "static/images/5-1.png", "selectedIconPath": "static/images/5-2.png", "text": "留言評論" } ] }, "requiredBackgroundModes": ["audio"] }
  • App.vue文件 (本人主要是為了增加項目更新后的提醒),所以在這個文件加了些相關內容,內容如下:
<script>
export default { onLaunch () { // 檢測小程序是否有新版本更新 if (wx.canIUse('getUpdateManager')) { const updateManager = wx.getUpdateManager() updateManager.onCheckForUpdate(function (res) { // 請求完新版本信息的回調 if (res.hasUpdate) { updateManager.onUpdateReady(function () { wx.showModal({ title: '更新提示', content: '新版本已經準備好,是否重啟應用?', success: function (res) { if (res.confirm) { // 新的版本已經下載好,調用 applyUpdate 應用新版本並重啟  updateManager.applyUpdate() } } }) }) // 小程序有新版本,會主動觸發下載操作(無需開發者觸發)  wx.getUpdateManager().onUpdateFailed(function () { // 當新版本下載失敗,會進行回調  wx.showModal({ title: '提示', content: '檢查到有新版本,下載失敗,請檢查網絡設置', showCancel: false }) }) } }) } else { // 版本過低則無法使用該方法  wx.showModal({ title: '提示', confirmColor: '#5BB53C', content: '當前微信版本過低,無法使用該功能,請升級到最新微信版本后重試。' }) } } } </script> <style lang="stylus"> page height 100% image display block </style>
  • main.js文件:
import Vue from 'vue'
import App from './App' Vue.config.productionTip = false App.mpType = 'app' wx.cloud.init({ env: '雲開發環境ID' }) const app = new Vue(App) app.$mount()
  • functions目錄:主要放一些雲函數,這裏不清楚雲函數的文章後面會提及
  • images目錄:主要放一些靜態資源圖片

頁面介紹

首頁——邀請函

首頁着重和大家講解下背景音樂的實現方法

const audioCtx = wx.createInnerAudioContext()

 接口獲取實例

接着,通過實例的相關方法來實現音樂的播放與暫停功能

具體代碼如下:

<script>
import IndexSwiper from 'components/indexSwiper' import tools from 'common/js/h_tools' const audioCtx = wx.createInnerAudioContext() export default { name: 'Index', components: { IndexSwiper }, data () { return { isPlay: true, list: [] } }, onShow () { const that = this that.isPlay = true that.getMusicUrl() }, methods: { audioPlay () { const that = this if (that.isPlay) { audioCtx.pause() that.isPlay = false tools.showToast('您已暫停音樂播放~') } else { audioCtx.play() that.isPlay = true tools.showToast('背景音樂已開啟~') } }, getList () { const that = this const db = wx.cloud.database() const banner = db.collection('banner') banner.get().then(res => { that.list = res.data[0].bannerList }) }, getMusicUrl () { const that = this const db = wx.cloud.database() const music = db.collection('music') music.get().then(res => { let musicUrl = res.data[0].musicUrl audioCtx.src = musicUrl audioCtx.loop = true audioCtx.play() that.getList() }) } }, onShareAppMessage: function (res) { return { path: '/pages/index/main' } } } </script>

以上代碼中使用到了雲開發相關功能,文章後面會介紹,請大家稍安勿躁

相冊頁——就一個輪播圖,這裏就不過多介紹

地圖頁——這裏着重講一下地圖標籤map

map標籤 

這裏講一下標記點markers:

data () {
    return { // qqSdk: '',  markers: [{ iconPath: '../../static/images/nav.png', id: 0, latitude: 30.08059, longitude: 115.93027, width: 50, height: 50 }] } }
<template>
    <div class="map">
        <image mode="aspectFit" class="head-img" src="../../static/images/t1.png"/>
        <map class="content" id="map" longitude="115.93027" latitude="30.08059" :markers="markers" scale="18" @tap="toNav">
        </map>
        <div class="call">
            <div class="left" @tap="linkHe">
                <image src="../../static/images/he.png"/>
                <span>呼叫新郎</span>
            </div>
            <div class="right" @tap="linkShe">
                <image src="../../static/images/she.png"/>
                <span>呼叫新娘</span>
            </div>
        </div>
        <image class="footer" src="../../static/images/grren-flower-line.png"/>
    </div>
</template>

祝福頁——也是雲開發相關內容,後面會介紹

留言頁——也是雲開發相關內容,後面會介紹

雲開發介紹

project.config.json文件:

"cloudfunctionRoot": "static/functions/"

進行雲開發首先我們需要找到上面這個文件,在上面這個json文件中加上上面這行代碼

cloudfunctionRoot 用於指定存放雲函數的目錄

app.json文件:

"window": {
    "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle": "black" }, "cloud": true

增加字段 "cloud": true實現雲開發能力的兼容性

開通雲開發

在開發者工具工具欄左側,點擊 “雲開發” 按鈕即可開通雲開發

雲開發控制台

數據庫

 雲開發提供了一個 JSON 數據庫

 

存儲

 

雲開發提供了一塊存儲空間,提供了上傳文件到雲端、帶權限管理的雲端下載能力,開發者可以在小程序端和雲函數端通過 API 使用雲存儲功能。

 

雲函數

 

雲函數是一段運行在雲端的代碼,無需管理服務器,在開發工具內編寫、一鍵上傳部署即可運行後端代碼。

使用雲開發

雲能力初始化

在小程序端開始使用雲能力前,需先調用 wx.cloud.init 方法完成雲能力初始化

import Vue from 'vue'
import App from './App' Vue.config.productionTip = false App.mpType = 'app' wx.cloud.init({ env: '雲開發環境ID' }) const app = new Vue(App) app.$mount()

數據庫的使用

在開始使用數據庫 API 進行增刪改查操作之前,需要先獲取數據庫的引用。以下調用獲取默認環境的數據庫的引用:

const db = wx.cloud.database()

 要操作一個集合,需先獲取它的引用:

const todos = db.collection('todos')

操作數據庫的相關示例

例:首頁獲取背景音樂資源

getMusicUrl () {
      const that = this const db = wx.cloud.database() const music = db.collection('music') music.get().then(res => { let musicUrl = res.data[0].musicUrl audioCtx.src = musicUrl audioCtx.loop = true audioCtx.play() that.getList() }) }

 例:首頁獲取輪播圖數組

getList () {
      const that = this const db = wx.cloud.database() const banner = db.collection('banner') banner.get().then(res => { that.list = res.data[0].bannerList }) }

例:祝福頁,用戶送上祝福存儲用戶

<script>
import tools from 'common/js/h_tools' export default { name: 'Greet', data () { return { userList: [], openId: '', userInfo: '' } }, onShow () { const that = this that.getUserList() }, methods: { scroll (e) { console.log(e) }, sendGreet (e) { const that = this if (e.target.errMsg === 'getUserInfo:ok') { wx.getUserInfo({ success: function (res) { that.userInfo = res.userInfo that.getOpenId() } }) } }, addUser () { const that = this const db = wx.cloud.database() const user = db.collection('user') user.add({ data: { user: that.userInfo } }).then(res => { that.getUserList() }) }, getOpenId () { const that = this wx.cloud.callFunction({ name: 'user', data: {} }).then(res => { that.openId = res.result.openid that.getIsExist() }) }, getIsExist () { const that = this const db = wx.cloud.database() const user = db.collection('user') user.where({ _openid: that.openId }).get().then(res => { if (res.data.length === 0) { that.addUser() } else { tools.showToast('您已經送過祝福了~') } }) }, getUserList () { const that = this wx.cloud.callFunction({ name: 'userList', data: {} }).then(res => { that.userList = res.result.data.reverse() }) } } } </script>

獲取送祝福的好友列表

getUserList () {
      const that = this wx.cloud.callFunction({ name: 'userList', data: {} }).then(res => { that.userList = res.result.data.reverse() }) }

這裏用到了雲函數,之所以用雲函數是因為小程序端在獲取集合數據時服務器一次默認並且最多返回 20 條記錄,雲函數端這個数字則是 100。

雲函數的使用方法

上面我們講過在project.config.json文件中配置雲函數存放位置

下面是雲函數messageList的index.js文件:

不成熟寫法:

const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database() const MAX_LIMIT = 100 exports.main = async (event, context) => { // 先取出集合記錄總數 const countResult = await db.collection('message').count() const total = countResult.total // 計算需分幾次取 const batchTimes = Math.ceil(total / 100) // 承載所有讀操作的 promise 的數組 const tasks = [] for (let i = 0; i < batchTimes; i++) { const promise = db.collection('message').skip(i * MAX_LIMIT).limit(MAX_LIMIT).get() tasks.push(promise) } // 等待所有 return (await Promise.all(tasks)).reduce((acc, cur) => ({ data: acc.data.concat(cur.data), errMsg: acc.errMsg })) }

成熟寫法(分頁查詢):

const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database() exports.main = async (event, context) => { const wxContext = cloud.getWXContext() const dbName = 'message' const filter = event.filter ? event.filter : null const pageNum = event.pageNum ? event.pageNum : 1 const pageSize = event.pageSize ? event.pageSize : 10 const countResult = await db.collection(dbName).where(filter).count() const total = countResult.total const totalPage = Math.ceil(total / pageSize) let hasMore if (pageNum >= totalPage) { hasMore = false } else { hasMore = true } return db.collection(dbName).orderBy('time', 'desc').where(filter).skip((pageNum - 1) * pageSize).limit(pageSize).get().then(res => { res.hasMore = hasMore res.total = total res.openId = wxContext.OPENID return res }) }

使用雲函數前,在開發者工具上,找到messageList目錄,右鍵如圖:

 點擊上傳並部署:雲端安裝依賴(不上傳node_modules)

得到如圖的提示:

 安裝完點擊完成就能使用當前雲函數了,使用方法即:

getUserList () {
      const that = this wx.cloud.callFunction({ name: 'userList', data: {} }).then(res => { that.userList = res.result.data.reverse() }) }

數組之所以要倒序是因為希望新祝福的的用戶在最前面显示 

用戶送上祝福的時候存儲用戶

這裏我們用到了雲函數獲取用戶信息,

當小程序端調用雲函數時,雲函數的傳入參數中會被注入小程序端用戶的 openid,開發者無需校驗 openid 的正確性,因為微信已經完成了這部分鑒權,開發者可以直接使用該 openid

不成熟寫法:

下面是雲函數user的index.js文件:

// 雲函數入口文件
const cloud = require('wx-server-sdk')

cloud.init()

// 雲函數入口函數
exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID } }

主要是為了獲取當前操作用戶的openid,獲取當前用戶的openid方法:

getOpenId () {
      const that = this wx.cloud.callFunction({ name: 'user', data: {} }).then(res => { that.openId = res.result.openid that.getIsExist() }) }

接着判斷當前用戶是否已經存在於數據庫中,即getIsExist()方法:

getIsExist () {
      const that = this const db = wx.cloud.database() const user = db.collection('user') user.where({ _openid: that.openId }).get().then(res => { if (res.data.length === 0) { that.addUser() } else { tools.showToast('您已經送過祝福了~') } }) }

如果得到的數組長度為零則添加改用戶到數據庫,否則則提醒當前用戶已經送過祝福

接下來介紹存儲用戶信息的方法,即addUser():

addUser () {
      const that = this const db = wx.cloud.database() const user = db.collection('user') user.add({ data: { user: that.userInfo } }).then(res => { that.getUserList() }) }

存入到數據庫的信息是這樣的:

成熟寫法(使用雲函數一次搞定):

// 雲函數入口文件
const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database() exports.main = async (event, context) => { const wxContext = cloud.getWXContext() const dbName = 'user' const filter = { _openid: wxContext.OPENID } const countResult = await db.collection(dbName).where(filter).count() const total = countResult.total if (total) { return { has: true } } else { return db.collection(dbName).add({ data: { user: event.user, _openid: wxContext.OPENID, time: db.serverDate() } }) } }
toMessage (e) {
      const that = this
      if (e.target.errMsg === 'getUserInfo:ok') { wx.getUserInfo({ success: function (res) { that.userInfo = res.userInfo wx.navigateTo({ url: `/pages/writeMessage/main?avatar=${that.userInfo.avatarUrl}&name=${that.userInfo.nickName}`  }) that.addUser(that.userInfo) } }) } }, addUser (obj) { wx.cloud.callFunction({ name: 'addUser', data: { user: obj } }) }

總結 

大概的功能就是這麼多,希望可以幫助到大家,覺得寫得不錯的記得給作者點個贊,你們的支持是我不斷更新的最大動力!

源碼地址

後續優化

  • 留言審核
  • 一些動畫效果
  • 分頁處理
  • 雲函數優化
  • 回到頂部

最後

一開始不清楚隨筆和文章的區別,還是希望這篇隨筆能展示在博客園首頁(所以改成隨筆),讓更多的朋友看到

希望對那些有想法又不會後台開發的朋友一些啟示作用,祝你早日做出只屬於自己的小程序。

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

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

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

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

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

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

湖北禁用廚餘垃圾餵豬 違規可罰5萬元

摘錄自2020年1月7日星島日報報導

中國非洲豬瘟疫情持續,湖北省政府立法,直接使用廚餘垃圾餵豬,將罰款最高5萬元人民幣,將於3月1日起實施。

政府指,為加強廚餘垃圾管理,保障食品安全,促進廚餘垃圾資源化利用和無害化處理,所以實施《湖北省餐廚垃圾管理辦法》規定。根據規定,在廚餘垃圾投放、收集、運輸、處置過程中,不得實施下列行為:將廚餘垃圾與其他生活垃圾混合投放;將廚餘垃圾排入污水排水管道、雨水管道等市政公共設施以及河道、湖泊、水庫等公共水域;隨意傾倒、拋撒廚餘垃圾等。

另外亦規定直接使用廚餘垃圾餵豬的,由縣級以上政府農業農村主管部門責令停止違法行為,可以處5000元以上2萬元以下罰款;情節嚴重的,處2萬元以上5萬元以下罰款。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

傳蘋果電動車將於 2019 年問世

蘋果公司生產汽車傳聞已久,美國《華爾街日報》21 日報導,蘋果正全力研發電動車,希望在 2019 年問世。   報導指出,蘋果電動車計劃的代號為「泰坦」(Titan),車款類型則像廂型休旅車。蘋果除了已聘請無人駕駛領域的專家外,也將泰坦的開發團隊由 600 人擴大 3 倍至 1,800 人,且可能會與傳統車廠合作,但蘋果官方發言人,拒絕對此消息作出評論。   外媒 AppleInsider 上周曾指出,蘋果已向電動車生產商特斯拉招兵買馬。 雖然有報導稱蘋果聘請無人駕駛領域的專家,但首款汽車仍需人操控駕駛。

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

【其他文章推薦】

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

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

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

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

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