重學 Java 設計模式:實戰中介者模式「按照Mybatis原理手寫ORM框架,給JDBC方式操作數據庫增加中介者場景」

作者:小傅哥
博客:https://bugstack.cn – 原創系列專題文章

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言

同齡人的差距是從什麼時候拉開的

同樣的幼兒園、同樣的小學、一樣的書本、一樣的課堂,有人學習好、有人學習差。不只是上學,幾乎人生處處都是賽道,發令槍響起的時刻,也就把人生的差距拉開。編程開發這條路也是很長很寬,有人跑得快有人跑得慢。那麼你是否想起過,這一點點的差距到遙不可及的距離,是從哪一天開始的。摸摸肚子的肉,看看遠處的路,別人講的是故事,你想起的都是事故

思想沒有產品高才寫出一片的ifelse

當你承接一個需求的時候,比如;交易、訂單、營銷、保險等各類場景。如果你不熟悉這個場景下的業務模式,以及將來的拓展方向,那麼很難設計出良好可擴展的系統。再加上產品功能初建,說老闆要的急,儘快上線。作為程序員的你更沒有時間思考,整體一看現在的需求也不難,直接上手開干(一個方法兩個if語句),這樣確實滿足了當前需求。但老闆的想法多呀,產品也跟着變化快,到你這就是改改改,加加加。當然你也不客氣,回首掏就是1024個if語句!

日積月累的技術沉澱是為了厚積薄發

粗略的估算過,如果從上大學開始每天寫200行,一個月是6000行,一年算10個月話,就是6萬行,第三年出去實習的是時候就有20萬行的代碼量。如果你能做到這一點,找工作難?有時候很多事情就是靠時間積累出來的,想走捷徑有時候真的沒有。你的技術水平、你的業務能力、你身上的肉,都是一點點積累下來的,不要浪費看似很短的時間,一年年堅持下來,留下印刻青春的痕迹,多給自己武裝上一些能力。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. mysql 5.1.20
  4. 涉及工程一個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-16-01 使用JDBC方式連接數據庫
itstack-demo-design-16-02 手寫ORM框架操作數據庫

三、中介者模式介紹

中介者模式要解決的就是複雜功能應用之間的重複調用,在這中間添加一層中介者包裝服務,對外提供簡單、通用、易擴展的服務能力。

這樣的設計模式幾乎在我們日常生活和實際業務開發中都會見到,例如;飛機降落有小姐姐在塔台喊話、無論哪個方向來的候車都從站台上下、公司的系統中有一个中台專門為你包裝所有接口和提供統一的服務等等,這些都運用了中介者模式。除此之外,你用到的一些中間件,他們包裝了底層多種數據庫的差異化,提供非常簡單的方式進行使用。

四、案例場景模擬

在本案例中我們通過模仿Mybatis手寫ORM框架,通過這樣操作數據庫學習中介者運用場景

除了這樣的中間件層使用場景外,對於一些外部接口,例如N種獎品服務,可以由中台系統進行統一包裝對外提供服務能力。也是中介者模式的一種思想體現。

在本案例中我們會把jdbc層進行包裝,讓用戶在使用數據庫服務的時候,可以和使用mybatis一樣簡單方便,通過這樣的源碼方式學習中介者模式,也方便對源碼知識的拓展學習,增強知識棧。

五、用一坨坨代碼實現

這是一種關於數據庫操作最初的方式

基本上每一個學習開發的人都學習過直接使用jdbc方式連接數據庫,進行CRUD操作。以下的例子可以當做回憶。

1. 工程結構

itstack-demo-design-16-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── JDBCUtil.java
  • 這裏的類比較簡單隻包括了一個數據庫操作類。

2. 代碼實現

public class JDBCUtil {

    private static Logger logger = LoggerFactory.getLogger(JDBCUtil.class);

    public static final String URL = "jdbc:mysql://127.0.0.1:3306/itstack-demo-design";
    public static final String USER = "root";
    public static final String PASSWORD = "123456";

    public static void main(String[] args) throws Exception {
        //1. 加載驅動程序
        Class.forName("com.mysql.jdbc.Driver");
        //2. 獲得數據庫連接
        Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
        //3. 操作數據庫
        Statement stmt = conn.createStatement();
        ResultSet resultSet = stmt.executeQuery("SELECT id, name, age, createTime, updateTime FROM user");
        //4. 如果有數據 resultSet.next() 返回true
        while (resultSet.next()) {
            logger.info("測試結果 姓名:{} 年齡:{}", resultSet.getString("name"),resultSet.getInt("age"));
        }
    }

}
  • 以上是使用JDBC的方式進行直接操作數據庫,幾乎大家都使用過這樣的方式。

3. 測試結果

15:38:10.919 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:水水 年齡:18
15:38:10.922 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:豆豆 年齡:18
15:38:10.922 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:花花 年齡:19

Process finished with exit code 0
  • 從測試結果可以看到這裏已經查詢到了數據庫中的數據。只不過如果在全部的業務開發中都這樣實現,會非常的麻煩。

六、中介模式開發ORM框架

`接下來就使用中介模式的思想完成模仿Mybatis的ORM框架開發~

1. 工程結構

itstack-demo-design-16-02
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── dao
    │   │       │	├── ISchool.java
    │   │       │	└── IUserDao.java
    │   │       ├── mediator
    │   │       │	├── Configuration.java
    │   │       │	├── DefaultSqlSession.java
    │   │       │	├── DefaultSqlSessionFactory.java
    │   │       │	├── Resources.java
    │   │       │	├── SqlSession.java
    │   │       │	├── SqlSessionFactory.java
    │   │       │	├── SqlSessionFactoryBuilder.java
    │   │       │	└── SqlSessionFactoryBuilder.java
    │   │       └── po
    │   │         	├── School.java
    │   │         	└── User.java
    │   └── resources
    │       ├── mapper
    │       │   ├── School_Mapper.xml
    │       │   └── User_Mapper.xml
    │       └── mybatis-config-datasource.xml
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

中介者模式模型結構

  • 以上是對ORM框架實現的核心類,包括了;加載配置文件、對xml解析、獲取數據庫session、操作數據庫以及結果返回。
  • 左上是對數據庫的定義和處理,基本包括我們常用的方法;<T> T selectOne<T> List<T> selectList等。
  • 右側藍色部分是對數據庫配置的開啟session的工廠處理類,這裏的工廠會操作DefaultSqlSession
  • 之後是紅色地方的SqlSessionFactoryBuilder,這個類是對數據庫操作的核心類;處理工廠、解析文件、拿到session等。

接下來我們就分別介紹各個類的功能實現過程。

2. 代碼實現

2.1 定義SqlSession接口

public interface SqlSession {

    <T> T selectOne(String statement);

    <T> T selectOne(String statement, Object parameter);

    <T> List<T> selectList(String statement);

    <T> List<T> selectList(String statement, Object parameter);

    void close();
}
  • 這裏定義了對數據庫操作的查詢接口,分為查詢一個結果和查詢多個結果,同時包括有參數和沒有參數的方法。

2.2 SqlSession具體實現類

public class DefaultSqlSession implements SqlSession {

    private Connection connection;
    private Map<String, XNode> mapperElement;

    public DefaultSqlSession(Connection connection, Map<String, XNode> mapperElement) {
        this.connection = connection;
        this.mapperElement = mapperElement;
    }

    @Override
    public <T> T selectOne(String statement) {
        try {
            XNode xNode = mapperElement.get(statement);
            PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
            ResultSet resultSet = preparedStatement.executeQuery();
            List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
            return objects.get(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public <T> List<T> selectList(String statement) {
        XNode xNode = mapperElement.get(statement);
        try {
            PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
            ResultSet resultSet = preparedStatement.executeQuery();
            return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    // ...

    private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
        List<T> list = new ArrayList<>();
        try {
            ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            // 每次遍歷行值
            while (resultSet.next()) {
                T obj = (T) clazz.newInstance();
                for (int i = 1; i <= columnCount; i++) {
                    Object value = resultSet.getObject(i);
                    String columnName = metaData.getColumnName(i);
                    String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
                    Method method;
                    if (value instanceof Timestamp) {
                        method = clazz.getMethod(setMethod, Date.class);
                    } else {
                        method = clazz.getMethod(setMethod, value.getClass());
                    }
                    method.invoke(obj, value);
                }
                list.add(obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

    @Override
    public void close() {
        if (null == connection) return;
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  • 這裏包括了接口定義的方法實現,也就是包裝了jdbc層。
  • 通過這樣的包裝可以讓對數據庫的jdbc操作隱藏起來,外部調用的時候對入參、出參都有內部進行處理。

2.3 定義SqlSessionFactory接口

public interface SqlSessionFactory {

    SqlSession openSession();

}
  • 開啟一個SqlSession, 這幾乎是大家在平時的使用中都需要進行操作的內容。雖然你看不見,但是當你有數據庫操作的時候都會獲取每一次執行的SqlSession

2.4 SqlSessionFactory具體實現類

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private final Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
    }

}
  • DefaultSqlSessionFactory,是使用mybatis最常用的類,這裏我們簡單的實現了一個版本。
  • 雖然是簡單的版本,但是包括了最基本的核心思路。當開啟SqlSession時會進行返回一個DefaultSqlSession
  • 這個構造函數中向下傳遞了Configuration配置文件,在這個配置文件中包括;Connection connectionMap<String, String> dataSourceMap<String, XNode> mapperElement。如果有你閱讀過Mybatis源碼,對這個就不會陌生。

2.5 SqlSessionFactoryBuilder實現

public class SqlSessionFactoryBuilder {

    public DefaultSqlSessionFactory build(Reader reader) {
        SAXReader saxReader = new SAXReader();
        try {
            saxReader.setEntityResolver(new XMLMapperEntityResolver());
            Document document = saxReader.read(new InputSource(reader));
            Configuration configuration = parseConfiguration(document.getRootElement());
            return new DefaultSqlSessionFactory(configuration);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return null;
    }

    private Configuration parseConfiguration(Element root) {
        Configuration configuration = new Configuration();
        configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
        configuration.setConnection(connection(configuration.dataSource));
        configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
        return configuration;
    }

    // 獲取數據源配置信息
    private Map<String, String> dataSource(List<Element> list) {
        Map<String, String> dataSource = new HashMap<>(4);
        Element element = list.get(0);
        List content = element.content();
        for (Object o : content) {
            Element e = (Element) o;
            String name = e.attributeValue("name");
            String value = e.attributeValue("value");
            dataSource.put(name, value);
        }
        return dataSource;
    }

    private Connection connection(Map<String, String> dataSource) {
        try {
            Class.forName(dataSource.get("driver"));
            return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    // 獲取SQL語句信息
    private Map<String, XNode> mapperElement(List<Element> list) {
        Map<String, XNode> map = new HashMap<>();

        Element element = list.get(0);
        List content = element.content();
        for (Object o : content) {
            Element e = (Element) o;
            String resource = e.attributeValue("resource");

            try {
                Reader reader = Resources.getResourceAsReader(resource);
                SAXReader saxReader = new SAXReader();
                Document document = saxReader.read(new InputSource(reader));
                Element root = document.getRootElement();
                //命名空間
                String namespace = root.attributeValue("namespace");

                // SELECT
                List<Element> selectNodes = root.selectNodes("select");
                for (Element node : selectNodes) {
                    String id = node.attributeValue("id");
                    String parameterType = node.attributeValue("parameterType");
                    String resultType = node.attributeValue("resultType");
                    String sql = node.getText();

                    // ? 匹配
                    Map<Integer, String> parameter = new HashMap<>();
                    Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                    Matcher matcher = pattern.matcher(sql);
                    for (int i = 1; matcher.find(); i++) {
                        String g1 = matcher.group(1);
                        String g2 = matcher.group(2);
                        parameter.put(i, g2);
                        sql = sql.replace(g1, "?");
                    }

                    XNode xNode = new XNode();
                    xNode.setNamespace(namespace);
                    xNode.setId(id);
                    xNode.setParameterType(parameterType);
                    xNode.setResultType(resultType);
                    xNode.setSql(sql);
                    xNode.setParameter(parameter);

                    map.put(namespace + "." + id, xNode);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }

        }
        return map;
    }

}
  • 在這個類中包括的核心方法有;build(構建實例化元素)parseConfiguration(解析配置)dataSource(獲取數據庫配置)connection(Map<String, String> dataSource) (鏈接數據庫)mapperElement (解析sql語句)
  • 接下來我們分別介紹這樣的幾個核心方法。

build(構建實例化元素)

這個類主要用於創建解析xml文件的類,以及初始化SqlSession工廠類DefaultSqlSessionFactory。另外需要注意這段代碼saxReader.setEntityResolver(new XMLMapperEntityResolver());,是為了保證在不聯網的時候一樣可以解析xml,否則會需要從互聯網獲取dtd文件。

parseConfiguration(解析配置)

是對xml中的元素進行獲取,這裏主要獲取了;dataSourcemappers,而這兩個配置一個是我們數據庫的鏈接信息,另外一個是對數據庫操作語句的解析。

connection(Map<String, String> dataSource) (鏈接數據庫)

鏈接數據庫的地方和我們常見的方式是一樣的;Class.forName(dataSource.get("driver"));,但是這樣包裝以後外部是不需要知道具體的操作。同時當我們需要鏈接多套數據庫的時候,也是可以在這裏擴展。

mapperElement (解析sql語句)

這部分代碼塊內容相對來說比較長,但是核心的點就是為了解析xml中的sql語句配置。在我們平常的使用中基本都會配置一些sql語句,也有一些入參的佔位符。在這裏我們使用正則表達式的方式進行解析操作。

解析完成的sql語句就有了一個名稱和sql的映射關係,當我們進行數據庫操作的時候,這個組件就可以通過映射關係獲取到對應sql語句進行操作。

3. 測試驗證

在測試之前需要導入sql語句到數據庫中;

  • 庫名:itstack-demo-design
  • 表名:userschool
CREATE TABLE school ( id bigint NOT NULL AUTO_INCREMENT, name varchar(64), address varchar(256), createTime datetime, updateTime datetime, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into school (id, name, address, createTime, updateTime) values (1, '北京大學', '北京市海淀區頤和園路5號', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
insert into school (id, name, address, createTime, updateTime) values (2, '南開大學', '中國天津市南開區衛津路94號', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
insert into school (id, name, address, createTime, updateTime) values (3, '同濟大學', '上海市彰武路1號同濟大廈A樓7樓7區', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
CREATE TABLE user ( id bigint(11) NOT NULL AUTO_INCREMENT, name varchar(32), age int(4), address varchar(128), entryTime datetime, remark varchar(64), createTime datetime, updateTime datetime, status int(4) DEFAULT '0', dateTime varchar(64), PRIMARY KEY (id), INDEX idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (1, '水水', 18, '吉林省榆樹市黑林鎮尹家村5組', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200309');
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (2, '豆豆', 18, '遼寧省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 1, null);
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (3, '花花', 19, '遼寧省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200310');

3.1 創建數據庫對象類

用戶類

public class User {

    private Long id;
    private String name;
    private Integer age;
    private Date createTime;
    private Date updateTime;
    
    // ... get/set
}

學校類

public class School {

    private Long id;
    private String name;
    private String address;
    private Date createTime;
    private Date updateTime;  
  
    // ... get/set
}
  • 這兩個類都非常簡單,就是基本的數據庫信息。

3.2 創建DAO包

用戶Dao

public interface IUserDao {

     User queryUserInfoById(Long id);

}

學校Dao

public interface ISchoolDao {

    School querySchoolInfoById(Long treeId);

}

3.3 ORM配置文件

鏈接配置

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack_demo_design?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
        <mapper resource="mapper/School_Mapper.xml"/>
    </mappers>

</configuration>
  • 這個配置與我們平常使用的mybatis基本是一樣的,包括了數據庫的連接池信息以及需要引入的mapper映射文件。

操作配置(用戶)

<mapper namespace="org.itstack.demo.design.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.design.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where id = #{id}
    </select>

    <select id="queryUserList" parameterType="org.itstack.demo.design.po.User" resultType="org.itstack.demo.design.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where age = #{age}
    </select>

</mapper>

操作配置(學校)

<mapper namespace="org.itstack.demo.design.dao.ISchoolDao">

    <select id="querySchoolInfoById" resultType="org.itstack.demo.design.po.School">
        SELECT id, name, address, createTime, updateTime
        FROM school
        where id = #{id}
    </select>

</mapper>

3.4 單個結果查詢測試

@Test
public void test_queryUserInfoById() {
    String resource = "mybatis-config-datasource.xml";
    Reader reader;
    try {
        reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
        try {
            User user = session.selectOne("org.itstack.demo.design.dao.IUserDao.queryUserInfoById", 1L);
            logger.info("測試結果:{}", JSON.toJSONString(user));
        } finally {
            session.close();
            reader.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 這裏的使用方式和Mybatis是一樣的,都包括了;資源加載和解析、SqlSession工廠構建、開啟SqlSession以及最後執行查詢操作selectOne

測試結果

16:56:51.831 [main] INFO  org.itstack.demo.design.demo.ApiTest - 測試結果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}

Process finished with exit code 0
  • 從結果上看已經滿足了我們的查詢需求。

3.5 集合結果查詢測試

@Test
public void test_queryUserList() {
    String resource = "mybatis-config-datasource.xml";
    Reader reader;
    try {
        reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
        try {
            User req = new User();
            req.setAge(18);
            List<User> userList = session.selectList("org.itstack.demo.design.dao.IUserDao.queryUserList", req);
            logger.info("測試結果:{}", JSON.toJSONString(userList));
        } finally {
            session.close();
            reader.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 這個測試內容與以上只是查詢方法有所不同;session.selectList,是查詢一個集合結果。

測試結果

16:58:13.963 [main] INFO  org.itstack.demo.design.demo.ApiTest - 測試結果:[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]

Process finished with exit code 0
  • 測試驗證集合的結果也是正常的,目前位置測試全部通過。

七、總結

  • 以上通過中介者模式的設計思想我們手寫了一個ORM框架,隱去了對數據庫操作的複雜度,讓外部的調用方可以非常簡單的進行操作數據庫。這也是我們平常使用的Mybatis的原型,在我們日常的開發使用中,只需要按照配置即可非常簡單的操作數據庫。
  • 除了以上這種組件模式的開發外,還有服務接口的包裝也可以使用中介者模式來實現。比如你們公司有很多的獎品接口需要在營銷活動中對接,那麼可以把這些獎品接口統一收到中台開發一個獎品中心,對外提供服務。這樣就不需要每一個需要對接獎品的接口,都去找具體的提供者,而是找中台服務即可。
  • 在上述的實現和測試使用中可以看到,這種模式的設計滿足了;單一職責開閉原則,也就符合了迪米特原則,即越少人知道越好。外部的人只需要按照需求進行調用,不需要知道具體的是如何實現的,複雜的一面已經有組件合作服務平台處理。

八、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式「多種類型商品不同接口,統一發獎服務搭建場景」
  • 2. 重學 Java 設計模式:實戰原型模式「上機考試多套試,每人題目和答案亂序排列場景」
  • 3. 重學 Java 設計模式:實戰橋接模式「多支付渠道(微信、支付寶)與多支付模式(刷臉、指紋)場景」
  • 4. 重學 Java 設計模式:實戰組合模式「營銷差異化人群發券,決策樹引擎搭建場景」
  • 5. 重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」
  • 6. 重學 Java 設計模式:實戰享元模式「基於Redis秒殺,提供活動與庫存信息查詢場景」

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

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

為什麼建議你使用枚舉?

枚舉是 JDK 1.5 新增的數據類型,使用枚舉我們可以很好的描述一些特定的業務場景,比如一年中的春、夏、秋、冬,還有每周的周一到周天,還有各種顏色,以及可以用它來描述一些狀態信息,比如錯誤碼等。

枚舉類型不止存在在 Java 語言中,在其它語言中也都能找到它的身影,例如 C# 和 Python 等,但我發現在實際的項目中使用枚舉的人很少,所以本文就來聊一聊枚舉的相關內容,好讓朋友們對枚舉有一個大概的印象,這樣在編程時起碼還能想到有“枚舉”這樣一個類型。

本文的結構目錄如下:

枚舉的 7 種使用方法

很多人不使用枚舉的一個重要的原因是對枚舉不夠熟悉,那麼我們就先從枚舉的 7 種使用方法說起。

用法一:常量

在 JDK 1.5 之前,我們定義常量都是 public static final... ,但有了枚舉,我們就可以把這些常量定義成一個枚舉類了,實現代碼如下:

public enum ColorEnum {  
  RED, GREEN, BLANK, YELLOW  
} 

用法二:switch

將枚舉用在 switch 判斷中,使得代碼可讀性更高了,實現代碼如下:

enum ColorEnum {
    GREEN, YELLOW, RED
}
public class ColorTest {
    ColorEnum color = ColorEnum.RED;

    public void change() {
        switch (color) {
            case RED:
                color = ColorEnum.GREEN;
                break;
            case YELLOW:
                color = ColorEnum.RED;
                break;
            case GREEN:
                color = ColorEnum.YELLOW;
                break;
        }
    }
}

用法三:枚舉中增加方法

我們可以在枚舉中增加一些方法,讓枚舉具備更多的特性,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ErrorCodeEnum errorCode = ErrorCodeEnum.SUCCESS;
        System.out.println("狀態碼:" + errorCode.code() + 
                           " 狀態信息:" + errorCode.msg());
    }
}

enum ErrorCodeEnum {
    SUCCESS(1000, "success"),
    PARAM_ERROR(1001, "parameter error"),
    SYS_ERROR(1003, "system error"),
    NAMESPACE_NOT_FOUND(2001, "namespace not found"),
    NODE_NOT_EXIST(3002, "node not exist"),
    NODE_ALREADY_EXIST(3003, "node already exist"),
    UNKNOWN_ERROR(9999, "unknown error");

    private int code;
    private String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int code() {
        return code;
    }

    public String msg() {
        return msg;
    }

    public static ErrorCodeEnum getErrorCode(int code) {
        for (ErrorCodeEnum it : ErrorCodeEnum.values()) {
            if (it.code() == code) {
                return it;
            }
        }
        return UNKNOWN_ERROR;
    }
}

以上程序的執行結果為:

狀態碼:1000 狀態信息:success

用法四:覆蓋枚舉方法

我們可以覆蓋一些枚舉中的方法用於實現自己的業務,比如我們可以覆蓋 toString() 方法,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ColorEnum colorEnum = ColorEnum.RED;
        System.out.println(colorEnum.toString());
    }
}

enum ColorEnum {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    //  成員變量
    private String name;
    private int index;

    //  構造方法
    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }

    //覆蓋方法
    @Override
    public String toString() {
        return this.index + ":" + this.name;
    }
}

以上程序的執行結果為:

1:紅色

用法五:實現接口

枚舉類可以用來實現接口,但不能用於繼承類,因為枚舉默認繼承了 java.lang.Enum 類,在 Java 語言中允許實現多接口,但不能繼承多個父類,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ColorEnum colorEnum = ColorEnum.RED;
        colorEnum.print();
        System.out.println("顏色:" + colorEnum.getInfo());
    }
}

interface Behaviour {
    void print();

    String getInfo();
}

enum ColorEnum implements Behaviour {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    private String name;
    private int index;

    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }

    @Override
    public void print() {
        System.out.println(this.index + ":" + this.name);
    }

    @Override
    public String getInfo() {
        return this.name;
    }
}

以上程序的執行結果為:

1:紅色

顏色:紅色

用法六:在接口中組織枚舉類

我們可以在一個接口中創建多個枚舉類,用它可以很好的實現“多態”,也就是說我們可以將擁有相同特性,但又有細微實現差別的枚舉類聚集在一個接口中,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        // 賦值第一個枚舉類
        ColorInterface colorEnum = ColorInterface.ColorEnum.RED;
        System.out.println(colorEnum);
        // 賦值第二個枚舉類
        colorEnum = ColorInterface.NewColorEnum.NEW_RED;
        System.out.println(colorEnum);
    }
}

interface ColorInterface {
    enum ColorEnum implements ColorInterface {
        GREEN, YELLOW, RED
    }
    enum NewColorEnum implements ColorInterface {
        NEW_GREEN, NEW_YELLOW, NEW_RED
    }
}

以上程序的執行結果為:

RED

NEW_RED

用法七:使用枚舉集合

在 Java 語言中和枚舉類相關的,還有兩個枚舉集合類 java.util.EnumSetjava.util.EnumMap,使用它們可以實現更多的功能。

使用 EnumSet 可以保證元素不重複,並且能獲取指定範圍內的元素,示例代碼如下:

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

public class EnumTest {
    public static void main(String[] args) {
        List<ColorEnum> list = new ArrayList<ColorEnum>();
        list.add(ColorEnum.RED);
        list.add(ColorEnum.RED);  // 重複元素
        list.add(ColorEnum.YELLOW);
        list.add(ColorEnum.GREEN);
        // 去掉重複數據
        EnumSet<ColorEnum> enumSet = EnumSet.copyOf(list);
        System.out.println("去重:" + enumSet);

        // 獲取指定範圍的枚舉(獲取所有的失敗狀態)
        EnumSet<ErrorCodeEnum> errorCodeEnums = EnumSet.range(ErrorCodeEnum.ERROR, ErrorCodeEnum.UNKNOWN_ERROR);
        System.out.println("所有失敗狀態:" + errorCodeEnums);
    }
}

enum ColorEnum {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    private String name;
    private int index;

    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }
}

enum ErrorCodeEnum {
    SUCCESS(1000, "success"),
    ERROR(2001, "parameter error"),
    SYS_ERROR(2002, "system error"),
    NAMESPACE_NOT_FOUND(2003, "namespace not found"),
    NODE_NOT_EXIST(3002, "node not exist"),
    NODE_ALREADY_EXIST(3003, "node already exist"),
    UNKNOWN_ERROR(9999, "unknown error");

    private int code;
    private String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int code() {
        return code;
    }

    public String msg() {
        return msg;
    }
}

以上程序的執行結果為:

去重:[RED, GREEN, YELLOW]

所有失敗狀態:[ERROR, SYS_ERROR, NAMESPACE_NOT_FOUND, NODE_NOT_EXIST, NODE_ALREADY_EXIST, UNKNOWN_ERROR]

EnumMapHashMap 類似,不過它是一個專門為枚舉設計的 Map 集合,相比 HashMap 來說它的性能更高,因為它內部放棄使用鏈表和紅黑樹的結構,採用數組作為數據存儲的結構。

EnumMap 基本使用示例如下:

import java.util.EnumMap;

public class EnumTest {
    public static void main(String[] args) {
        EnumMap<ColorEnum, String> enumMap = new EnumMap<>(ColorEnum.class);
        enumMap.put(ColorEnum.RED, "紅色");
        enumMap.put(ColorEnum.GREEN, "綠色");
        enumMap.put(ColorEnum.BLANK, "白色");
        enumMap.put(ColorEnum.YELLOW, "黃色");
        System.out.println(ColorEnum.RED + ":" + enumMap.get(ColorEnum.RED));
    }
}

enum ColorEnum {
    RED, GREEN, BLANK, YELLOW;
}

以上程序的執行結果為:

RED:紅色

使用注意事項

阿里《Java開發手冊》對枚舉的相關規定如下,我們在使用時需要稍微注意一下。

【強制】所有的枚舉類型字段必須要有註釋,說明每個數據項的用途。

【參考】枚舉類名帶上 Enum 後綴,枚舉成員名稱需要全大寫,單詞間用下劃線隔開。說明:枚舉其實就是特殊的常量類,且構造方法被默認強制是私有。正例:枚舉名字為 ProcessStatusEnum 的成員名稱:SUCCESS / UNKNOWN_REASON。

假如不使用枚舉

在枚舉沒有誕生之前,也就是 JDK 1.5 版本之前,我們通常會使用 int 常量來表示枚舉,實現代碼如下:

public static final int COLOR_RED = 1;
public static final int COLOR_BLUE = 2;
public static final int COLOR_GREEN = 3;

但是使用 int 類型可能存在兩個問題:

第一, int 類型本身並不具備安全性,假如某個程序員在定義 int 時少些了一個 final 關鍵字,那麼就會存在被其他人修改的風險,而反觀枚舉類,它“天然”就是一個常量類,不存在被修改的風險(原因詳見下半部分);

第二,使用 int 類型的語義不夠明確,比如我們在控制台打印時如果只輸出 1…2…3 這樣的数字,我們肯定不知道它代表的是什麼含義。

那有人就說了,那就使用常量字符唄,這總不會還不知道語義吧?實現示例代碼如下:

public static final String COLOR_RED = "RED";
public static final String COLOR_BLUE = "BLUE";
public static final String COLOR_GREEN = "GREEN";

但是這樣同樣存在一個問題,有些初級程序員會不按套路出牌,他們可能會直接使用字符串的值進行比較,而不是直接使用枚舉的字段,實現示例代碼如下:

public class EnumTest {
    public static final String COLOR_RED = "RED";
    public static final String COLOR_BLUE = "BLUE";
    public static final String COLOR_GREEN = "GREEN";
    public static void main(String[] args) {
        String color = "BLUE";
        if ("BLUE".equals(color)) {
            System.out.println("藍色");
        }
    }
}

這樣當我們修改了枚舉中的值,那程序就涼涼了。

枚舉使用場景

枚舉的常見使用場景是單例,它的完整實現代碼如下:

public class Singleton {
    // 枚舉類型是線程安全的,並且只會裝載一次
    private enum SingletonEnum {
        INSTANCE;
        // 聲明單例對象
        private final Singleton instance;
        // 實例化
        SingletonEnum() {
            instance = new Singleton();
        }
        private Singleton getInstance() {
            return instance;
        }
    }
    // 獲取實例(單例對象)
    public static Singleton getInstance() {
        return SingletonEnum.INSTANCE.getInstance();
    }
    private Singleton() {
    }
    // 類方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}
class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.sayHi();
    }
}

因為枚舉只會在類加載時裝載一次,所以它是線程安全的,這也是《Effective Java》作者極力推薦使用枚舉來實現單例的主要原因。

知識擴展

枚舉為什麼是線程安全的?

這一點要從枚舉最終生成的字節碼說起,首先我們先來定義一個簡單的枚舉類:

public enum ColorEnumTest {
    RED, GREEN, BLANK, YELLOW;
}

然後我們再將上面的那段代碼編譯為字節碼,具體內容如下:

public final class ColorEnumTest extends java.lang.Enum<ColorEnumTest> {
  public static final ColorEnumTest RED;
  public static final ColorEnumTest GREEN;
  public static final ColorEnumTest BLANK;
  public static final ColorEnumTest YELLOW;
  public static ColorEnumTest[] values();
  public static ColorEnumTest valueOf(java.lang.String);
  static {};
}

從上述結果可以看出枚舉類最終會被編譯為被 final 修飾的普通類,它的所有屬性也都會被 staticfinal 關鍵字修飾,所以枚舉類在項目啟動時就會被 JVM 加載並初始化,而這個執行過程是線程安全的,所以枚舉類也是線程安全的類。

小貼士:代碼反編譯的過程是先用 javac 命令將 java 代碼編譯字節碼(.class),再使用 javap 命令查看編譯的字節碼。

枚舉比較小技巧

我們在枚舉比較時使用 == 就夠了,因為枚舉類是在程序加載時就創建了(它並不是 new 出來的),並且枚舉類不允許在外部直接使用 new 關鍵字來創建枚舉實例,所以我們在使用枚舉類時本質上只有一個對象,因此在枚舉比較時使用 == 就夠了。

並且我們在查看枚舉的 equlas() 源碼會發現,它的內部其實還是直接調用了 == 方法,源碼如下:

public final boolean equals(Object other) {
    return this==other;
}

總結

本文我們介紹了枚舉類的 7 種使用方法:常量、switch、枚舉中添加方法、覆蓋枚舉方法、實現接口、在接口中組織枚舉類和使用枚舉集合等,然後講了如果不使用枚舉類使用 int 類型和 String 類型存在的一些弊端:語義不夠清晰、容易被修改、存在被誤用的風險,所以我們在適合的環境下應該盡量使用枚舉類。並且我們還講了枚舉類的使用場景——單例,以及枚舉類為什麼是安全的,最後我們講了枚舉比較的小技巧,希望本文對你有幫助。

查看 & 鳴謝

https://www.iteye.com/blog/softbeta-1185573

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

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

特斯拉將公布儲能電池計畫 F-貿聯可望受惠

特斯拉傳將於 4 月 30 日公布儲能電池計畫,市場推測,該計畫將包括住宅與公共規模市場,業界傳出,本來就是特斯拉重要夥伴的 F-貿聯,有機會獲新電力線訂單。而今年在特斯拉需求持續強勁,與 Type C 等外接式擴充介面的需求爆發下,法人預期,該公司今年營運將逐季走高,全年營收看增 15%。   F-貿聯首季營收 19.04 億元,年增 10%,創下歷年同期新高。而特斯拉傳出將於下周公布最新的儲能電池計畫,據市場傳言表示,貿聯本來就是特斯拉在電動車電池模組用線與超級充電樁的線束的主力供應商,未來不排除大型公共規模用儲能電池,將與現有充電站結合。而貿聯有機會順勢切入取得新產品,並以公用市場應用的電力線為主,近日有望已小量出貨。   法人預估,F-貿聯今年除來自特斯拉的需求成長外,另在其他車用佈局方面,包括全地形車大客戶全車線束、擴大歐美客戶於倒車雷達等新產品的開發;另外針對美國官方對公營單位節能環保的需求,也配合客戶開發出得以改裝現有車為電動車的配備裝置,隨未來該商業模式成型,貢獻將逐步擴大。  

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

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

最新的一波Vue實戰技巧,不用則已,一用驚人

Vue中,不同的選項有不同的合併策略,比如 data,props,methods是同名屬性覆蓋合併,其他直接合併,而生命周期鈎子函數則是將同名的函數放到一個數組中,在調用的時候依次調用

Vue中,提供了一個api, Vue.config.optionMergeStrategies,可以通過這個api去自定義選項的合併策略。

在代碼中打印

console.log(Vue.config.optionMergeStrategies)

  

 通過合併策略自定義生命周期函數

背景

最近客戶給領導反饋,我們的系統用一段時間,瀏覽器就變得有點卡,不知道為什麼。問題出來了,本來想甩鍋到後端,但是瀏覽器問題,沒法甩鍋啊,那就排查吧。

後來發現頁面有許多定時器,ajax輪詢還有動畫,打開一個瀏覽器頁簽沒法問題,打開多了,瀏覽器就變得卡了,這時候我就想如果能在用戶切換頁簽時候將這些都停掉,不久解決了。百度裏面上下檢索,找到了一個事件visibilitychange,可以用來判斷瀏覽器頁簽是否显示。

有方法了,就寫唄

export default {
  created() {
    window.addEventListener('visibilitychange', this.$_hanldeVisiblityChange)
    // 此處用了hookEvent,可以參考小編前一篇文章
    this.$on('hook:beforeDestroy', () => {
      window.removeEventListener(
        'visibilitychange',
        this.$_hanldeVisiblityChange
      )
    })
  },
  methods: {
    $_hanldeVisiblityChange() {
      if (document.visibilityState === 'hidden') {
        // 停掉那一堆東西
      }
      if (document.visibilityState === 'visible') {
        // 開啟那一堆東西
      }
    }
  }
}

通過上面的代碼,可以看到在每一個需要監聽處理的文件都要寫一堆事件監聽,判斷頁面是否显示的代碼,一處兩處還可以,文件多了就頭疼了,這時候小編突發奇想,定義一個頁面显示隱藏的生命周期鈎子,把這些判斷都封裝起來

自定義生命周期鈎子函數

定義生命周期函數 pageHidden 與 pageVisible

import Vue from 'vue'

// 通知所有組件頁面狀態發生了變化
const notifyVisibilityChange = (lifeCycleName, vm) => {
  // 生命周期函數會存在$options中,通過$options[lifeCycleName]獲取生命周期
  const lifeCycles = vm.$options[lifeCycleName]
  // 因為使用了created的合併策略,所以是一個數組
  if (lifeCycles && lifeCycles.length) {
    // 遍歷 lifeCycleName對應的生命周期函數列表,依次執行
    lifeCycles.forEach(lifecycle => {
      lifecycle.call(vm)
    })
  }
  // 遍歷所有的子組件,然後依次遞歸執行
  if (vm.$children && vm.$children.length) {
    vm.$children.forEach(child => {
      notifyVisibilityChange(lifeCycleName, child)
    })
  }
}

// 添加生命周期函數
export function init() {
  const optionMergeStrategies = Vue.config.optionMergeStrategies
  // 定義了兩個生命周期函數 pageVisible, pageHidden
  // 為什麼要賦值為 optionMergeStrategies.created呢
  // 這個相當於指定 pageVisible, pageHidden 的合併策略與 created的相同(其他生命周期函數都一樣)
  optionMergeStrategies.pageVisible = optionMergeStrategies.beforeCreate
  optionMergeStrategies.pageHidden = optionMergeStrategies.created
}


// 將事件變化綁定到根節點上面
// rootVm vue根節點實例
export function bind(rootVm) {
  window.addEventListener('visibilitychange', () => {
    // 判斷調用哪個生命周期函數
    let lifeCycleName = undefined
    if (document.visibilityState === 'hidden') {
      lifeCycleName = 'pageHidden'
    } else if (document.visibilityState === 'visible') {
      lifeCycleName = 'pageVisible'
    }
    if (lifeCycleName) {
      // 通過所有組件生命周期發生變化了
      notifyVisibilityChange(lifeCycleName, rootVm)
    }
  })
}

應用

  1. main.js主入口文件引入
import { init, bind } from './utils/custom-life-cycle'

// 初始化生命周期函數, 必須在Vue實例化之前確定合併策略
init()

const vm = new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

// 將rootVm 綁定到生命周期函數監聽裏面
bind(vm)

  2. 在需要的地方監聽生命周期函數

export default {
  pageVisible() {
    console.log('頁面显示出來了')
  },
  pageHidden() {
    console.log('頁面隱藏了')
  }
}

  

provideinject,不止父子傳值,祖宗傳值也可以

Vue相關的面試經常會被面試官問道,Vue父子之間傳值的方式有哪些,通常我們會回答,props傳值,$emit事件傳值,vuex傳值,還有eventbus傳值等等,今天再加一種provideinject傳值,離offer又近了一步。(對了,下一節還有一種)

使用過React的同學都知道,在React中有一個上下文Context,組件可以通過Context向任意後代傳值,而Vueprovideinject的作用於Context的作用基本一樣

先舉一個例子

使用過elemment-ui的同學一定對下面的代碼感到熟悉

<template>
  <el-form :model="formData" size="small">
    <el-form-item label="姓名" prop="name">
      <el-input v-model="formData.name" />
    </el-form-item>
    <el-form-item label="年齡" prop="age">
      <el-input-number v-model="formData.age" />
    </el-form-item>
    <el-button>提交</el-button>
  </el-form>
</template>
<script>
export default {
  data() {
    return {
      formData: {
        name: '',
        age: 0
      }
    }
  }
}
</script>

  

看了上面的代碼,貌似沒啥特殊的,天天寫啊。在el-form上面我們指定了一個屬性size="small",然後有沒有發現表單裏面的所有表單元素以及按鈕的 size都變成了small,這個是怎麼做到的?接下來我們自己手寫一個表單模擬一下

自己手寫一個表單

自定義表單custom-form.vue

<template>
  <form class="custom-form">
    <slot></slot>
  </form>
</template>
<script>
export default {
  props: {
    // 控製表單元素的大小
    size: {
      type: String,
      default: 'default',
      // size 只能是下面的四個值
      validator(value) {
        return ['default', 'large', 'small', 'mini'].includes(value)
      }
    },
    // 控製表單元素的禁用狀態
    disabled: {
      type: Boolean,
      default: false
    }
  },
  // 通過provide將當前表單實例傳遞到所有後代組件中
  provide() {
    return {
      customForm: this
    }
  }
}
</script>

  

在上面代碼中,我們通過provide將當前組件的實例傳遞到後代組件中,provide是一個函數,函數返回的是一個對象

自定義表單項custom-form-item.vue

沒有什麼特殊的,只是加了一個label,element-ui更複雜一些

<template>
  <div class="custom-form-item">
    <label class="custom-form-item__label">{{ label }}</label>
    <div class="custom-form-item__content">
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    label: {
      type: String,
      default: ''
    }
  }
}
</script>

自定義輸入框 custom-input.vue

<template>
  <div
    class="custom-input"
    :class="[
      `custom-input--${getSize}`,
      getDisabled && `custom-input--disabled`
    ]"
  >
    <input class="custom-input__input" :value="value" @input="$_handleChange" />
  </div>
</template>
<script>
/* eslint-disable vue/require-default-prop */
export default {
  props: {
    // 這裏用了自定義v-model
    value: {
      type: String,
      default: ''
    },
    size: {
      type: String
    },
    disabled: {
      type: Boolean
    }
  },
  // 通過inject 將form組件注入的實例添加進來
  inject: ['customForm'],
  computed: {
    // 通過計算組件獲取組件的size, 如果當前組件傳入,則使用當前組件的,否則是否form組件的
    getSize() {
      return this.size || this.customForm.size
    },
    // 組件是否禁用
    getDisabled() {
      const { disabled } = this
      if (disabled !== undefined) {
        return disabled
      }
      return this.customForm.disabled
    }
  },
  methods: {
    // 自定義v-model
    $_handleChange(e) {
      this.$emit('input', e.target.value)
    }
  }
}
</script>

  


form中,我們通過
provide返回了一個對象,在
input中,我們可以通過
inject獲取
form中返回對象中的項,如上代碼
inject:['customForm']所示,然後就可以在組件內通過
this.customForm調用
form實例上面的屬性與方法了

在項目中使用

<template>
  <custom-form size="small">
    <custom-form-item label="姓名">
      <custom-input v-model="formData.name" />
    </custom-form-item>
  </custom-form>
</template>
<script>
import CustomForm from '../components/custom-form'
import CustomFormItem from '../components/custom-form-item'
import CustomInput from '../components/custom-input'
export default {
  components: {
    CustomForm,
    CustomFormItem,
    CustomInput
  },
  data() {
    return {
      formData: {
        name: '',
        age: 0
      }
    }
  }
}
</script>

  執行上面代碼,運行結果為:

<form class="custom-form">
  <div class="custom-form-item">
    <label class="custom-form-item__label">姓名</label>
    <div class="custom-form-item__content">
      <!--size=small已經添加到指定的位置了-->
      <div class="custom-input custom-input--small">
        <input class="custom-input__input">
      </div>
    </div>
  </div>
</form>

  

通過上面的代碼可以看到,input組件已經設置組件樣式為custom-input--small

inject格式說明

除了上面代碼中所使用的inject:['customForm']寫法之外,inject還可以是一個對象。且可以指定默認值

修改上例,如果custom-input外部沒有custom-form,則不會注入customForm,此時為customForm指定默認值

{
  inject: {
    customForm: {
      // 對於非原始值,和props一樣,需要提供一個工廠方法
      default: () => ({
        size: 'default'
      })
    }
  }
}

  

使用限制

1.provideinject的綁定不是可響應式的。但是,如果你傳入的是一個可監聽的對象,如上面的customForm: this,那麼其對象的屬性還是可響應的。

2.Vue官網建議provideinject 主要在開發高階插件/組件庫時使用。不推薦用於普通應用程序代碼中。因為provideinject在代碼中是不可追溯的(ctrl + f可以搜),建議可以使用Vuex代替。 但是,也不是說不能用,在局部功能有時候用了作用還是比較大的。

 

插槽,我要鑽到你的懷裡

插槽,相信每一位Vue都有使用過,但是如何更好的去理解插槽,如何去自定義插槽,今天小編為你帶來更形象的說明。

默認插槽

<template>
  <!--這是一個一居室-->
  <div class="one-bedroom">
    <!--添加一個默認插槽,用戶可以在外部隨意定義這個一居室的內容-->
    <slot></slot>
  </div>
</template>

  

<template>
  <!--這裏一居室-->
  <one-bedroom>
    <!--將傢具放到房間裏面,組件內部就是上面提供的默認插槽的空間-->
    <span>先放一個小床,反正沒有女朋友</span>
    <span>再放一個電腦桌,在家還要加班寫bug</span>
  </one-bedroom>
</template>
<script>
import OneBedroom from '../components/one-bedroom'
export default {
  components: {
    OneBedroom
  }
}
</script>

具名插槽

<template>
  <div class="two-bedroom">
    <!--這是主卧-->
    <div class="master-bedroom">
      <!---主卧使用默認插槽-->
      <slot></slot>
    </div>
    <!--這是次卧-->
    <div class="secondary-bedroom">
      <!--次卧使用具名插槽-->
      <slot name="secondard"></slot>
    </div>
  </div>
</template>

  

<template>
  <two-bedroom>
    <!--主卧使用默認插槽-->
    <div>
      <span>放一個大床,要結婚了,嘿嘿嘿</span>
      <span>放一個衣櫃,老婆的衣服太多了</span>
      <span>算了,還是放一個電腦桌吧,還要寫bug</span>
    </div>
    <!--次卧,通過v-slot:secondard 可以指定使用哪一個具名插槽, v-slot:secondard 也可以簡寫為 #secondard-->
    <template v-slot:secondard>
      <div>
        <span>父母要住,放一個硬一點的床,軟床對腰不好</span>
        <span>放一個衣櫃</span>
      </div>
    </template>
  </two-bedroom>
</template>
<script>
import TwoBedroom from '../components/slot/two-bedroom'
export default {
  components: {
    TwoBedroom
  }
}
</script>

作用域插槽

<template>
  <div class="two-bedroom">
    <!--其他內容省略-->
    <div class="toilet">
      <!--通過v-bind 可以向外傳遞參數, 告訴外面衛生間可以放洗衣機-->
      <slot name="toilet" v-bind="{ washer: true }"></slot>
    </div>
  </div>
</template>

  

<template>
  <two-bedroom>
    <!--其他省略-->
    <!--衛生間插槽,通過v-slot="scope"可以獲取組件內部通過v-bind傳的值-->
    <template v-slot:toilet="scope">
      <!--判斷是否可以放洗衣機-->
      <span v-if="scope.washer">這裏放洗衣機</span>
    </template>
  </two-bedroom>
</template>  

插槽默認值

<template>
  <div class="second-hand-house">
    <div class="master-bedroom">
      <!--插槽可以指定默認值,如果外部調用組件時沒有修改插槽內容,則使用默認插槽-->
      <slot>
        <span>這裡有一張水床,玩的夠嗨</span>
        <span>還有一個衣櫃,有點舊了</span>
      </slot>
    </div>
    <!--這是次卧-->
    <div class="secondary-bedroom">
      <!--次卧使用具名插槽-->
      <slot name="secondard">
        <span>這裡有一張嬰兒床</span>
      </slot>
    </div>
  </div>
</template>

  

<second-hand-house>
    <!--主卧使用默認插槽,只裝修主卧-->
    <div>
      <span>放一個大床,要結婚了,嘿嘿嘿</span>
      <span>放一個衣櫃,老婆的衣服太多了</span>
      <span>算了,還是放一個電腦桌吧,還要寫bug</span>
    </div>
  </second-hand-house>

dispatchbroadcast,這是一種有歷史的組件通信方式

dispatch
broadcast是一種有歷史的組件通信方式,為什麼是有歷史的,因為他們是
Vue1.0提供的一種方式,在
Vue2.0中廢棄了。但是廢棄了不代表我們不能自己手動實現,像許多UI庫內部都有實現。本文以
element-ui實現為基礎進行介紹。同時看完本節,你會對組件的
$parent,
$children,
$options有所了解。

方法介紹

$dispatch: $dispatch會向上觸發一個事件,同時傳遞要觸發的祖先組件的名稱與參數,當事件向上傳遞到對應的組件上時會觸發組件上的事件偵聽器,同時傳播會停止。

$broadcast: $broadcast會向所有的後代組件傳播一個事件,同時傳遞要觸發的後代組件的名稱與參數,當事件傳遞到對應的後代組件時,會觸發組件上的事件偵聽器,同時傳播會停止(因為向下傳遞是樹形的,所以只會停止其中一個恭弘=叶 恭弘子分支的傳遞)。

$dispatch實現與應用

1. 代碼實現

 // 向上傳播事件
 // @param {*} eventName 事件名稱
 // @param {*} componentName 接收事件的組件名稱
 // @param {...any} params 傳遞的參數,可以有多個
 
function dispatch(eventName, componentName, ...params) {
  // 如果沒有$parent, 則取$root
  let parent = this.$parent || this.$root
  while (parent) {
    // 組件的name存儲在組件的$options.componentName 上面
    const name = parent.$options.name
    // 如果接收事件的組件是當前組件
    if (name === componentName) {
      // 通過當前組件上面的$emit觸發事件,同事傳遞事件名稱與參數
      parent.$emit.apply(parent, [eventName, ...params])
      break
    } else {
      // 否則繼續向上判斷
      parent = parent.$parent
    }
  }
}

// 導出一個對象,然後在需要用到的地方通過混入添加
export default {
  methods: {
    $dispatch: dispatch
  }
}  

2. 代碼應用

  • 在子組件中通過$dispatch向上觸發事件

    import emitter from '../mixins/emitter'
    export default {
      name: 'Chart',
      // 通過混入將$dispatch加入進來
      mixins: [emitter],
       mounted() {
         // 在組件渲染完之後,將組件通過$dispatch將自己註冊到Board組件上
        this.$dispatch('register', 'Board', this)
      }
    }
  • Board組件上通過$on監聽要註冊的事件

$broadcast實現與應用

1. 代碼實現

  //向下傳播事件
  // @param {*} eventName 事件名稱
  // @param {*} componentName 要觸發組件的名稱
  // @param  {...any} params 傳遞的參數
 
function broadcast(eventName, componentName, ...params) {
  this.$children.forEach(child => {
    const name = child.$options.name
    if (name === componentName) {
      child.$emit.apply(child, [eventName, ...params])
    } else {
      broadcast.apply(child, [eventName, componentName, ...params])
    }
  })
}

// 導出一個對象,然後在需要用到的地方通過混入添加
export default {
  methods: {
    $broadcast: broadcast
  }
}  

2. 代碼應用

在父組件中通過$broadcast向下觸發事件

import emitter from '../mixins/emitter'
export default {
  name: 'Board',
  // 通過混入將$dispatch加入進來
  mixins: [emitter],
  methods:{
  	//在需要的時候,刷新組件
  	$_refreshChildren(params) {
  		this.$broadcast('refresh', 'Chart', params)
  	}
  }
}

在後代組件中通過$on監聽刷新事件

export default {
  name: 'Chart',
  created() {
    this.$on('refresh',(params) => {
      // 刷新事件
    })
  }
}

總結

通過上面的例子,同學們應該都能對$dispatch$broadcast有所了解,但是為什麼Vue2.0要放棄這兩個方法呢?官方給出的解釋是:”因為基於組件樹結構的事件流方式實在是讓人難以理解,並且在組件結構擴展的過程中會變得越來越脆弱。這種事件方式確實不太好,我們也不希望在以後讓開發者們太痛苦。並且 $dispatch$broadcast 也沒有解決兄弟組件間的通信問題。“

確實如官網所說,這種事件流的方式確實不容易讓人理解,而且後期維護成本比較高。但是在小編看來,不管黑貓白貓,能抓老鼠的都是好貓,在許多特定的業務場景中,因為業務的複雜性,很有可能使用到這樣的通信方式。但是使用歸使用,但是不能濫用,小編一直就在項目中有使用。

 

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

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

深入解讀Dictionary

Dictionary<TKey,TValue>是日常.net開發中最常用的數據類型之一,基本上遇到鍵值對類型的數據時第一反應就是使用這種散列表。散列表特別適合快速查找操作,查找的效率是常數階O(1)。那麼為什麼這種數據類型的查找效率能夠這麼高效?它背後的數據類型是如何支撐這種查找效率的?它在使用過程中有沒有什麼局限性?一起來探究下這個數據類型的奧秘吧。

本文內容針對的是.Net Framework 4.5.1的代碼實現,在其他.Net版本中或多或少都會有些差異,但是基本的原理還是相同的。

本文的內容主要分為三個部分,第一部分是從代碼的角度來分析並以圖文並茂的方式通俗的解釋Dictionary如何解決的散列衝突並實現高效的數據插入和查找。第二部分名為“眼見為實”,由於第一部分是從代碼層面分析Dictionary的實現,側重於理論分析,因此第二部分使用windbg直接分析內存結構,跟第一部分的理論分析相互印證,加深對於這種數據類型的深入理解。最後是從數據結構的時間複雜度的角度進行分析並提出了幾條實踐建議。

本文內容:

  • 第一部分 代碼分析
    • 散列衝突
    • Dictionary圖文解析
    • Dictionary的初始化
    • 添加第四個元素
  • 第二部分 眼見為實
    • 添加第一個元素后的內存結構
    • 添加第四個元素后的內存結構
  • 第三部分
    • 時間複雜度分析
    • 實踐建議

散列衝突

提到散列表,就不能不提散列衝突。由於哈希算法被計算的數據是無限的,而計算后的結果範圍有限,因此總會存在不同的數據經過計算后得到的值相同,這就是哈希衝突。(兩個不同的數據計算后的結果一樣)。散列衝突的解決方案有好幾個,比如開放尋址法、鏈式尋址法。

Dictionary使用的是鏈式尋址法,也叫做拉鏈法。拉鏈法的基本思想是將散列值相同的數據存在同一個鏈表中,如果有散列值相同的元素,則加到鏈表的頭部。同樣道理,在查找元素的時候,先計算散列值,然後在對應散列值的鏈表中查找目標元素。

用圖來表達鏈式尋址法的思想:

Dictionary<TKey,TValue>的內部數據結構

Dictionary的內部存儲數據主要是依賴了兩個數組,分別是int[] bucketsEntry[] entries。其中buckets是Dictionary所有操作的入口,類似於上文中解說拉鏈法所用的圖中的那個豎著的數據結構。Entry是一個結構體,用於封裝所有的元素並且增加了next字段用於構建鏈表數據結構。為了便於理解,下文中截取了一段相關的源代碼並增加了註釋。

//數據在Dictionary<TKey,TValue>的存儲形式,所有的鍵值對在Dictionary<TKey,TValue>都是被包裝成一個個的Entry保存在內存中
private struct Entry {
    public int hashCode;    // 散列計算結果的低31位數,默認值為-1
    public int next;        // 下一個Entry的索引值,鏈表的最後一個Entry的next為-1
    public TKey key;        // Entry對象的Key對應於傳入的TKey
    public TValue value;    // Entry對象的Value對應與傳入的TValue
}

private int[] buckets;      //hashCode的桶,是查找所有Entry的第一級數據結構
private Entry[] entries;    //保存真正的數據

下文中以Dictionary<int,string>為例,分析Dictionary在使用過程中內部數據的組織方式。

Dictionary初始化

初始化代碼:

Dictionary<int, string> dictionary = new Dictionary<int, string>();

Dictionary的初始化時,bucketsentries的長度都是0。

添加一個元素

dictionary.Add(1, "xyxy");

向Dictionary中添加這個新元素大致經過了7個步驟:

  1. 首先判斷數組長度是否需要擴容(原長度為0,需要擴容);
  2. 對於數組進行擴容,分別創建長度為3的bucket數組和entries數組(使用大於原數組長度2倍的第一個素數作為新的數組長度);
  3. 整數1的hashcode為1;
  4. 取低31位數值為1(計算公式:hashcode & 0x7FFFFFFF=1);
  5. 該key的hashcode落到bucket下標為1的位置(計算公式:hashCode % buckets.Length=1);
  6. 將hashcode、key、value包裝起來(封裝到entries數組下標為0的結構體中);
  7. 設置bucket[1]的值為0(因為新元素被封裝到了entries數組下標為0的位置);

當向Dictionary中添加一個元素后,內部數據結構如下圖(為了便於理解,圖上將bucket和entries中各個鏈表頭結點用線標出了關聯關係):

添加第二個元素

dictionary.Add(7, "xyxy");

向Dictionary中添加這個元素大致經過了6個步驟:

  1. 計算7的hashcode是7;
  2. 取低31位數值為7(計算公式:hashcode & 0x7FFFFFFF=1);
  3. 該key的hashcode落到bucket下標為1的位置(計算公式:hashCode % buckets.Length=1);
  4. 將hashcode、key、value包裝起來(封裝到entries數組下標為1的結構體中,跟步驟3計算得到的1沒有關係,只是因為entries數組下標為1的元素是空着的所以放在這裏);
  5. 原bucket[1]為0,所以設置當前結構體的Entry.next為0;
  6. 設置bucket[1]為1(因為鏈表的頭部節點位於entries數組下標為1的位置)

當向Dictionary中添加第二個元素后,內部數據結構是這樣的:

添加第三個元素

dictionary.Add(2, "xyxy");

向Dictionary添加這個元素經過了如下5個步驟:

  1. 整數2計算的hashcode是2;
  2. hashcode取低31位數值為2(計算公式:hashcode & 0x7FFFFFFF=2);
  3. 該key的hashcode落到bucket下標為2的位置(計算公式:hashCode % buckets.Length=2);
  4. 將hashcode、key、value包裝起來(封裝到entries數組下標為2的結構體中,到此entries的數組就存滿了);
  5. 原bucket[2]上為-1,所以bucket[2]節點下並沒有對應鏈表,設置當前結構體的Entry.next為-1;
  6. 設置bucket[2]為2(因為鏈表的頭部節點位於entries數組下標為2的位置)

當向Dictionary中添加第三個元素后,內部數據結構:

添加第四個元素

dictionary.Add(4, "xyxy");

通過前面幾個操作可以看出,當前數據結構中entries數組中的元素已滿,如果再添加元素的話,會發生怎樣的變化呢?

假如再對於dictionary添加一個元素,原來申請的內存空間肯定是不夠用的,必須對於當前數據結構進行擴容,然後在擴容的基礎上再執行添加元素的操作。那麼在解釋這個Add方法原理的時候,分為兩個場景分別進行:數組擴容和元素添加。

數組擴容

在發現數組容量不夠的時候,Dictionary首先執行擴容操作,擴容的規則與該數據類型首次初始化的規則相同,即使用大於原數組長度2倍的第一個素數7作為新數組的長度(3*2=6,大於6的第一個素數是7)。

擴容步驟:

  1. 新申請一個容量為7的數組,並將原數組的元素拷貝至新數組(代碼:Array.Copy(entries, 0, newEntries, 0, count);
  2. 重新計算原Dictionary中的元素的hashCode在bucket中的位置(注意新的bucket數組中數值的變化);
  3. 重新計算鏈表(注意entries數組中結構體的next值的變化);

擴容完成后Dictionary的內容數據結構:

添加元素

當前已經完成了entriesbucket數組的擴容,有了充裕的空間來存儲新的元素,所以可以在新的數據結構的基礎上繼續添加元素。

當向Dictionary中添加第四個元素后,內部數據結構是這樣的:

添加這個新的元素的步驟:

  1. 整數4計算的hashcode是4;
  2. hashcode取低31位數值為4(計算公式:hashcode & 0x7FFFFFFF=4);
  3. 該key的hashcode落到bucket下標為4的位置(計算公式:hashCode % buckets.Length=4);
  4. 將hashcode、key、value包裝起來;(封裝到entries數組下標為3的結構體中);
  5. 原bucket[4]上為-1,所以當前節點下並沒有鏈表,設置當前結構體的Entry.next為-1;
  6. 設置bucket[4]為3(因為鏈表的頭部節點位於entries數組下標為3的位置)

眼見為實

畢竟本文的主題是圖文並茂分析Dictionary<Tkey,Tvalue>的原理,雖然已經從代碼層面和理論層面分析了Dictionary<Tkey,Tvalue>的實現,但是如果能夠分析這個數據類型的實際內存數據結果,可以獲得更直觀的感受並且對於這個數據類型能夠有更加深入的認識。由於篇幅的限制,無法將Dictionary<Tkey,Tvalue>的所有操作場景結果都進行內存分析,那麼本文中精選有代表性的兩個場景進行分析:一是該數據類型初始化后添加第一個元素的內存結構,二是該數據類型進行第一次擴容后的數據結構。

Dictionary添加第一個元素后的內存結構

執行代碼:

Dictionary<int, string> dic = new Dictionary<int, string>();
dic.Add(1, "xyxy");
Console.Read();

打開windbg附加到該進程(由於使用的是控制台應用程序,當前線程是0號線程,因此如果附加進程后默認的不是0號線程時執行~0s切換到0號線程),執行!clrstack -l查看當前線程及線程上使用的所有變量:

0:000> !clrstack -l
OS Thread Id: 0x48b8 (0)
        Child SP               IP Call Site
0000006de697e998 00007ffab577c134 [InlinedCallFrame: 0000006de697e998] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
0000006de697e998 00007ffa96abc9c8 [InlinedCallFrame: 0000006de697e998] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
0000006de697e960 00007ffa96abc9c8 *** ERROR: Module load completed but symbols could not be loaded for C:\WINDOWS\assembly\NativeImages_v4.0.30319_64\mscorlib\5c1b7b73113a6f079ae59ad2eb210951\mscorlib.ni.dll
DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)

0000006de697ea40 00007ffa972d39ec System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef)
    LOCALS:
        <no data>
        <no data>
        <no data>
        <no data>
        <no data>
        <no data>

0000006de697ead0 00007ffa972d38f5 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
    LOCALS:
        <no data>
        <no data>

0000006de697eb30 00007ffa96a882d4 System.IO.StreamReader.ReadBuffer()
    LOCALS:
        <no data>
        <no data>

0000006de697eb80 00007ffa97275f23 System.IO.StreamReader.Read()
    LOCALS:
        <no data>

0000006de697ebb0 00007ffa9747a2fd System.IO.TextReader+SyncTextReader.Read()

0000006de697ec10 00007ffa97272698 System.Console.Read()

0000006de697ec40 00007ffa38670909 ConsoleTest.DictionaryDebug.Main(System.String[])
    LOCALS:
        0x0000006de697ec70 = 0x00000215680d2dd8

0000006de697ee88 00007ffa97ba6913 [GCFrame: 0000006de697ee88] 

通過對於線程堆棧的分析很容易看出當前線程上使用了一個局部變量,地址為:0x000001d86c972dd8,使用!do命令查看該變量的內容:

0:000> !do 0x00000215680d2dd8
Name:        System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib],[System.String, mscorlib]]
MethodTable: 00007ffa96513328
EEClass:     00007ffa9662f610
Size:        80(0x50) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffa964a8538  4001887        8       System.Int32[]  0 instance 00000215680d2ee8 buckets
00007ffa976c4dc0  4001888       10 ...non, mscorlib]][]  0 instance 00000215680d2f10 entries
00007ffa964a85a0  4001889       38         System.Int32  1 instance                1 count
00007ffa964a85a0  400188a       3c         System.Int32  1 instance                1 version
00007ffa964a85a0  400188b       40         System.Int32  1 instance               -1 freeList
00007ffa964a85a0  400188c       44         System.Int32  1 instance                0 freeCount
00007ffa96519630  400188d       18 ...Int32, mscorlib]]  0 instance 00000215680d2ed0 comparer
00007ffa964c6ad0  400188e       20 ...Canon, mscorlib]]  0 instance 0000000000000000 keys
00007ffa977214e0  400188f       28 ...Canon, mscorlib]]  0 instance 0000000000000000 values
00007ffa964a5dd8  4001890       30        System.Object  0 instance 0000000000000000 _syncRoot

從內存結構來看,該變量中就是我們查找的Dic存在buckets、entries、count、version等字段,其中buckets和entries在上文中已經有多次提及,也是本文的分析重點。既然要眼見為實,那麼buckets和entries這兩個數組的內容到底是什麼樣的呢?這兩個都是數組,一個是int數組,另一個是結構體數組,對於這兩個內容分別使用!da命令查看其內容:

首先是buckets的內容:

0:000> !da -start 0 -details 00000215680d2ee8 
Name:        System.Int32[]
MethodTable: 00007ffa964a8538
EEClass:     00007ffa966160e8
Size:        36(0x24) bytes
Array:       Rank 1, Number of elements 3, Type Int32
Element Methodtable: 00007ffa964a85a0
[0] 00000215680d2ef8
    Name:        System.Int32
    MethodTable: 00007ffa964a85a0
    EEClass:     00007ffa96616078
    Size:        24(0x18) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  40005a2        0             System.Int32      1     instance                   -1     m_value
[1] 00000215680d2efc
    Name:        System.Int32
    MethodTable: 00007ffa964a85a0
    EEClass:     00007ffa96616078
    Size:        24(0x18) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  40005a2        0             System.Int32      1     instance                    0     m_value
[2] 00000215680d2f00
    Name:        System.Int32
    MethodTable: 00007ffa964a85a0
    EEClass:     00007ffa96616078
    Size:        24(0x18) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  40005a2        0             System.Int32      1     instance                   -1     m_value


當前buckets中有三個值,分別是:-1、0和-1,其中-1是數組初始化后的默認值,而下錶為1的位置的值0則是上文中添加dic.Add(1, "xyxy");這個指令的結果,代表其對應的鏈表首節點在entries數組中下標為0的位置,那麼entries數組中的數值是什麼樣子的呢?

0:000> !da -start 0 -details 00000215680d2f10 
Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]][]
MethodTable: 00007ffa965135b8
EEClass:     00007ffa9662d1f0
Size:        96(0x60) bytes
Array:       Rank 1, Number of elements 3, Type VALUETYPE
Element Methodtable: 00007ffa96513558
[0] 00000215680d2f20
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513558
    EEClass:     00007ffa966304e8
    Size:        40(0x28) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  4003502        8             System.Int32      1     instance                    1     hashCode
        00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
        00007ffa964a85a0  4003504       10             System.Int32      1     instance                    1     key
        00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000215680d2db0     value
[1] 00000215680d2f38
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513558
    EEClass:     00007ffa966304e8
    Size:        40(0x28) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
        00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
        00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
        00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
[2] 00000215680d2f50
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513558
    EEClass:     00007ffa966304e8
    Size:        40(0x28) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
        00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
        00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
        00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value

通過對於entries數組的分析可以看出,這個數組也有三個值,其中下標為0的位置已經填入相關內容,比如hashCode為1,key為1,其中value的內容是一個內存地址:000001d86c972db0,這個地址指向的就是字符串對象,它的內容是xyxy,使用!do指令來看下具體內容:

0:000> !do  00000215680d2db0
Name:        System.String
MethodTable: 00007ffcc6b359c0
EEClass:     00007ffcc6b12ec0
Size:        34(0x22) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      xyxy
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffcc6b385a0  4000283        8         System.Int32  1 instance                4 m_stringLength
00007ffcc6b36838  4000284        c          System.Char  1 instance               78 m_firstChar
00007ffcc6b359c0  4000288       e0        System.String  0   shared           static Empty

簡要分析擴容后的內存結構

此次執行的代碼為:

Dictionary<int, string> dic = new Dictionary<int, string>();
dic.Add(1, "xyxy");
dic.Add(7, "xyxy");
dic.Add(2, "xyxy");
dic.Add(4, "xyxy");
Console.Read();

同樣採取附加進程的方式分析這段代碼執行后的內存結構,本章節中忽略掉如何查找Dictionary變量地址的部分,直接分析buckets數組和entries數組的內容。

首先是buckets數組的內存結構:

0:000> !da -start 0 -details 0000019a471a54f8 
Name:        System.Int32[]
MethodTable: 00007ffcc6b38538
EEClass:     00007ffcc6ca60e8
Size:        52(0x34) bytes
Array:       Rank 1, Number of elements 7, Type Int32
Element Methodtable: 00007ffcc6b385a0
[0] 0000019a471a5508
    Name:        System.Int32
    MethodTable: 00007ffcc6b385a0
    EEClass:     00007ffcc6ca6078
    Size:        24(0x18) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    1     m_value
[1] 0000019a471a550c
    Name:        System.Int32
    MethodTable: 00007ffcc6b385a0
    EEClass:     00007ffcc6ca6078
    Size:        24(0x18) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    0     m_value
[2] 0000019a471a5510
    Name:        System.Int32
    MethodTable: 00007ffcc6b385a0
    EEClass:     00007ffcc6ca6078
    Size:        24(0x18) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    2     m_value
[3] 0000019a471a5514
    Name:        System.Int32
    MethodTable: 00007ffcc6b385a0
    EEClass:     00007ffcc6ca6078
    Size:        24(0x18) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                   -1     m_value
[4] 0000019a471a5518
    Name:        System.Int32
    MethodTable: 00007ffcc6b385a0
    EEClass:     00007ffcc6ca6078
    Size:        24(0x18) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    3     m_value
[5] 0000019a471a551c
    Name:        System.Int32
    MethodTable: 00007ffcc6b385a0
    EEClass:     00007ffcc6ca6078
    Size:        24(0x18) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                   -1     m_value
[6] 0000019a471a5520
    Name:        System.Int32
    MethodTable: 00007ffcc6b385a0
    EEClass:     00007ffcc6ca6078
    Size:        24(0x18) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                   -1     m_value

然後是entries的內存結構:

0:000> !da -start 0 -details 00000237effb2fa8 
Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]][]
MethodTable: 00007ffa965135b8
EEClass:     00007ffa9662d1f0
Size:        192(0xc0) bytes
Array:       Rank 1, Number of elements 7, Type VALUETYPE
Element Methodtable: 00007ffa96513558
[0] 00000237effb2fb8
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513558
    EEClass:     00007ffa966304e8
    Size:        40(0x28) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  4003502        8             System.Int32      1     instance                    1     hashCode
        00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
        00007ffa964a85a0  4003504       10             System.Int32      1     instance                    1     key
        00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
[1] 00000237effb2fd0
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513558
    EEClass:     00007ffa966304e8
    Size:        40(0x28) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  4003502        8             System.Int32      1     instance                    7     hashCode
        00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
        00007ffa964a85a0  4003504       10             System.Int32      1     instance                    7     key
        00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
[2] 00000237effb2fe8
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513558
    EEClass:     00007ffa966304e8
    Size:        40(0x28) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  4003502        8             System.Int32      1     instance                    2     hashCode
        00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
        00007ffa964a85a0  4003504       10             System.Int32      1     instance                    2     key
        00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
[3] 00000237effb3000
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513558
    EEClass:     00007ffa966304e8
    Size:        40(0x28) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  4003502        8             System.Int32      1     instance                    4     hashCode
        00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
        00007ffa964a85a0  4003504       10             System.Int32      1     instance                    4     key
        00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
[4] 00000237effb3018
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513558
    EEClass:     00007ffa966304e8
    Size:        40(0x28) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
        00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
        00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
        00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
[5] 00000237effb3030
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513558
    EEClass:     00007ffa966304e8
    Size:        40(0x28) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
        00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
        00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
        00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
[6] 00000237effb3048
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513558
    EEClass:     00007ffa966304e8
    Size:        40(0x28) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
        00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
        00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
        00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value

從內存的結構來看,擴容后bucket數組中使用了下標為0、1、2和4這四個位置,entries中使用了0~3存儲了示例中添加的數據,符合前文中理論分析的結果,兩者相互之間具有良好的印證關係。

時間複雜度分析

時間複雜度表達的是數據結構操作數據的時候所消耗的時間隨着數據集規模的增長的變化趨勢。常用的指標有最好情況時間複雜度、最壞情況時間複雜度和均攤時間複雜度。那麼對於Dictionary<Tkey,TValue>來說,插入和查找過程中這些時間複雜度分別是什麼樣的呢?

最好情況時間複雜度:對於查找來說最好的是元素處於鏈表的頭部,查找效率不會隨着數據規模的增加而增加,因此該複雜度為常量階複雜度,即O(1);插入操作最理想的情況是數組中有空餘的空間,不需要進行擴容操作,此時時間複雜度也是常量階的,即O(1);

最壞情況時間複雜度:對於插入來說,比較耗時的操作場景是需要順着鏈表查找符合條件的元素,鏈表越長,查找時間越長(下文稱為場景一);而對於插入來說最壞的情況是數組長度不足,需要動態擴容並重新組織鏈表結構(下文稱為場景二);

場景一中時間複雜度隨着鏈表長度的增加而增加,但是Dictionary中規定鏈表的最大長度為100,如果有長度超過100的鏈表就需要擴容並調整鏈表結構,所以順着鏈表查找數據不會隨着數據規模的增長而增長,最大時間複雜度是固定的,因此時間複雜度還是常量階複雜度,即O(1);

場景二中時間複雜度隨着數組中元素的數量增加而增加,如果原來的數組元素為n個,那麼擴容時需要將這n個元素拷貝到新的數組中並計算其在新鏈表中的位置,因此該操作的時間複雜度是隨着數組的長度n的增加而增加的,屬於線性階時間複雜度,即O(n)。

綜合場景一和場景二的分析結果得出最壞情況時間複雜度出現在數據擴容過程中,時間複雜度為O(n)。

最好情況時間複雜度和最壞情況時間複雜度都過於極端,只能描述最好的情況和最壞的情況,那麼在使用過程中如何評價數據結構在大部分情況下的時間複雜度?通常對於複雜的數據結構可以使用均攤時間複雜度來評價這個指標。均攤時間複雜度適用於對於一個數據進行連續操作,大部分情況下時間複雜度都很低,只有個別情況下時間複雜度較高的場景。這些操作存在前後連貫性,這種情況下將較高的複雜度攤派到之前的操作中,一般均攤時間複雜度就相當於最好情況時間複雜度。

通過前面的分析可以看出Dictionary恰好就符合使用均攤時間複雜度分析的場景。以插入操作為例,假設預申請的entries數組長度為n,在第n+1次插入數據時必然會遇到一次數組擴容導致的時間消耗較高的場景。將這次較為複雜的操作的時間均攤到之前的n次操作后,每次操作時間的複雜度也是常量階的,因此Dictionary插入數據時均攤時間複雜度也是O(1)。

實踐建議

首先,Dictionary這種類型適合用於對於數據檢索有明顯目的性的場景,比如讀寫比例比較高的場景。其次,如果有大數據量的場景,最好能夠提前聲明容量,避免多次分配內存帶來的額外的時間和空間上的消耗。因為不進行擴容的場景,插入和查找效率都是常量階O(1),在引起擴容的情況下,時間複雜度是線性階O(n)。如果僅僅是為了存儲數據,使用Dictionary並不合適,因為它相對於List<T>具有更加複雜的數據結構,這樣會帶來額外的空間上面的消耗。雖然Dictionary<TKey,TValue>的TKey是個任意類型的,但是除非是對於判斷對象是否相等有特殊的要求,否則不建議直接使用自定義類作為Tkey。

總結

C#中的Dictionary<TKey,TValue>是藉助於散列函數構建的高性能數據結構,Dictionary解決散列衝突的方式是使用鏈表來保存散列值存在衝突的節點,俗稱拉鏈法。本文中通過圖文並茂的方式幫助理解Dictionary添加元素和擴容過程這兩個典型的應用場景,在理論分析之後使用windbg分析了內存中的實際結構,以此加深對於這種數據類型的深入理解。隨後在此基礎上分析了Dictionary的時間複雜度。Dictionary的最好情況時間複雜度是O(1),最壞情況複雜度是O(n),均攤時間複雜度是O(1),Dictionary在大多數情況下都是常量階時間複雜度,在內部數組自動擴容過程中會產生明顯的性能下降,因此在實際實踐過程中最好在聲明新對象的時候能夠預估容量,儘力避免數組自動擴容導致的性能下降。

參考資料

  • Dictionary<TKey,TValue>源代碼(.net framework4.8版本)
  • .NET中Dictionary<TKey, TValue>淺析
  • 解決hash衝突的三個方法
  • 算法複雜度分析(下):最好、最壞、平均、均攤等時間複雜度概述

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

解Bug之路-中間件”SQL重複執行”

前言

我們的分庫分表中間件在線上運行了兩年多,到目前為止還算穩定。在筆者將精力放在處理各種災難性事件(例如中間件物理機宕機/數據庫宕機/網絡隔離等突發事件)時。竟然發現還有一些奇怪的corner case。現在就將排查思路寫成文章分享出來。

Bug現場

應用拓撲

應用通過中間件連後端多個數據庫,sql會根據路由規則路由到指定的節點,如下圖所示:

錯誤現象

應用在做某些數據庫操作時,會發現有比較大的概率失敗。他們的代碼邏輯是這樣:

	int count = updateSql(sql1);
	...
	// 偽代碼
	int count = updateSql("update test set value =1 where id in ("100","200") and status = 1;
	if( 0 == count ){
		throw new RuntimeException("更新失敗");
	}
	......
	int count = updateSql(sql3);
	...

即每做一次update之後都檢查下是否更新成功,如果不成功則回滾並拋異常。
在實際測試的過程中,發現經常報錯,更新為0。而實際那條sql確實是可以更新到的(即報錯回滾后,我們手動執行sql可以執行並update count>0)。

中間件日誌

筆者根據sql去中間件日誌裏面搜索。發現了非常奇怪的結果,日誌如下:

2020-03-13 11:21:01:440 [NIOREACTOR-20-RW] frontIP=>ip1;sqlID=>12345678;rows=>0;sql=>update test set value =1 where id in ("1","2") and status = 1;start=>11:21:01:403;time=>24266;
2020-03-13 11:21:01:440 [NIOREACTOR-20-RW] frontIP=>ip1;sqlID=>12345678;rows=>2;sql=>update test set value =1 where id in ("1","2") and status = 1;start=>11:21:01:403;time=>24591;

由於中間件對每條sql都標識了唯一的一個sqlID,在日誌表現看來就好像sql執行了兩遍!由於sql中有一個in,很容易想到是否被拆成了兩條執行了。如下圖所示:

這條思路很快被筆者否決了,因為筆者explain並手動執行了一下,這條sql確實只路由到了一個節點。真正完全否決掉這條思路的是筆者在日誌裏面還發現,同樣的SQL會打印三遍!即看上去像執行了三次,這就和僅僅只in了兩個id的sql在思路上相矛盾了。

數據庫日誌

那到底數據真正執行了多少條呢?找DBA去撈一下其中的sql日誌,由於線下環境沒有日誌切割,日誌量巨大,搜索時間太慢。沒辦法,就按照現有的數據進行分析吧。

日誌如何被觸發

由於當前沒有任何思路,於是筆者翻看中間件的代碼,發現在update語句執行后,中間件會在收到mysql okay包后打印上述日誌。如下圖所示:

注意到所有出問題的update出問題的時候都是同一個NIOREACTOR線程先後打印了兩條日誌,所以筆者推斷這兩個okay大概率是同一個後端連接返回的。

什麼情況會返回多個okay?

這個問題筆者思索了很久,因為在筆者的實際重新執行出問題的sql並debug時,永遠只有一個okay返回。於是筆者聯想到,我們中間件有個狀態同步的部分,而這些狀態同步是將set auto_commit=0等sql拼接到應用發送的sql前面。即變成如下所示:

sql可能為
set auto_commit=0;set charset=gbk;>update test set value =1 where id in ("1","2") and status = 1;

於是筆者細細讀了這部分的代碼,發現處理的很好。其通過計算出前面拼接出的sql數量,再在接收okay包的時候進行遞減,最後將真正執行的那條sql處理返回。其處理如下圖所示:

但這裏確給了筆者一個靈感,即一條sql文本確實是有可能返回多個okay包的。

真相大白

在筆者發現(sql1;sql2;)這樣的拼接sql會返回多個okay包后,就立刻聯想到,該不會業務自己寫了這樣的sql發給中間件,造成中間件的sql處理邏輯錯亂吧。因為我們的中間件只有在對自己拼接(同步狀態)的sql做處理,明顯是無法處理應用傳過來即為拼接sql的情況。
由於看上去有問題的那條sql並沒有拼接,於是筆者憑藉這條sql打印所在的reactor線程往上搜索,發現其上面真的有拼接sql!

2020-03-1311:21:01:040[NIOREACTOR-20RW]frontIP=>ip1;sqlID=>12345678;rows=>1;
sql=>update test_2 set value =1 where id=1 and status = 1;update test_2 set value =1 where id=2 and status = 1;

如上圖所示,(update1;update2)中update1的okay返回被驅動認為是所有的返回。然後應用立即發送了update3。前腳剛發送,update2的okay返回就回來了而其剛好是0,應用就報錯了(要不是0,這個錯亂邏輯還不會提前暴露)。那三條”重複執行”也很好解釋了,就是之前的拼接sql會有三條。

為何是概率出現

但奇怪的是,並不是每次拼接sql都會造成update3″重複執行”的現象,按照筆者的推斷應該前面只要是多條拼接sql就會必現才對。於是筆者翻了下jdbc驅動源碼,發現其在發送命令之前會清理下接收buffer,如下所示:

MysqlIO.java
final Buffer sendCommand(......){
	......
	// 清理接收buffer,會將殘存的okay包清除掉
	clearInputStream();
	......
	send(this.sendPacket, this.sendPacket.getPosition());
	......
}

正是由於clearInputStream()使得錯誤非必現(暴露),如果okay(update2)在應用發送第三條sql前先到jdbc驅動會被驅動忽略!
讓我們再看一下不會讓update3″重複執行”的時序圖:

即根據okay(update2)返回的快慢來決定是否暴露這個問題,如下圖所示:

同時筆者觀察日誌,確實這種情況下”update1;update2″這條語句在中間件裏面日誌有兩條。

臨時解決方案

讓業務開發不用這些拼接sql的寫法后,再也沒出過問題。

為什麼不連中間件是okay的

業務開發這些sql是就在線上運行了好久,用了中間件后才出現問題。
既然不連中間件是okay的,那麼jdbc必然有這方面的完善處理,筆者去翻了下mysql-connect-java(5.1.46)。由於jdbc裏面存在大量的兼容細節處理,筆者這邊只列出一些關鍵代碼路徑:

MySQL JDBC 源碼
MySQLIO
stack;
executeUpdate
	|->executeUpdateInternel
		|->executeInternal
			|->execSQL
				|->sqlQueryDirect
					|->readAllResults (MysqlIO.java)
readAllResults: //核心在這個函數的處理裏面
ResultSetImpl readAllResults(......){
		......
       while (moreRowSetsExist) {
			  ......
			  // 在返回okay包的保中其serverStatus字段中如果SERVER_MORE_RESULTS_EXISTS置位
			  // 表明還有更多的okay packet
            moreRowSetsExist = (this.serverStatus & SERVER_MORE_RESULTS_EXISTS) != 0;
        }
        ......
}

正確的處理流程如下圖所示:

而我們中間件的源碼確實這麼處理的:

@Override
public void okResponse(byte[] data, BackendConnection conn) {
	......
	// 這邊僅僅處理了autocommit的狀態,沒有處理SERVER_MORE_RESULTS_EXISTS
	// 所以導致了不兼容拼接sql的現象
	ok.serverStatus = source.isAutocommit() ? 2 : 1;
	ok.write(source);
	......
}

select也”重複執行”了

解決完上面的問題后,筆者在日誌里竟然發現select盡然也有重複的,這邊並不會牽涉到okay包的處理,難道還有問題?日誌如下所示:

2020-03-13 12:21:01:040[NIOREACTOR-20RW]frontIP=>ip1;sqlID=>12345678;rows=>1;select abc;
2020-03-13 12:21:01:045[NIOREACTOR-21RW]frontIP=>ip2;sqlID=>12345678;rows=>1;select abc;

從不同的REACTOR線程號(20RW/21RW)和不同的frontIP(ip1,ip2)來看是兩個連接執行了同樣的sql,但為何sqlID是一樣的?任何一個詭異的現象都必須一查到底。於是筆者登錄到應用上看了下應用日誌,確實應用有兩個不同的線程運行了同一條sql。
那肯定是中間件日誌打印的問題了,筆者很快就想通了其中的關竅,我們中間件有個對同樣sql緩存其路由節點結構體的功能(這樣下一次同樣sql就不必解析,降低了CPU),而sqlID信息正好也在那個路由節點結構體裏面。如下圖所示:

這個緩存功能感覺沒啥用(因為線上基本是沒有相同sql的),於是筆者在筆者優化的閃電模式下(大幅度提高中間件性能)將這個功能禁用掉了,沒想到為了排查問題而開啟的詳細日誌碰巧將這個功能開啟了。

總結

任何系統都不能說百分之百穩定可靠,尤其是不能立flag。在線上運行了好幾年的系統也是如此。只有對所有預料外的現象進行細緻的追查與深入的分析並解決,才能讓我們的系統越來越可靠。

公眾號

關注筆者公眾號,獲取更多乾貨文章:

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

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

多圖解釋Redis的整數集合intset升級過程

redis源碼分析系列文章

[Redis源碼系列]在Liunx安裝和常見API 

為什麼要從Redis源碼分析 

String底層實現——動態字符串SDS 

雙向鏈表都不懂,還說懂Redis?

面試官:說說Redis的Hash底層 我:……(來自閱文的面試題)

Redis的跳躍表確定不了解下

 

前言

大噶好,今天仍然是元氣滿滿的一天,拋開永遠寫不完的需求,拒絕要求賊變態的客戶,單純的學習技術,感受技術的魅力。(哈哈哈,皮一下很開森)

前面幾周我們一起看了Redis底層數據結構,如動態字符串SDS雙向鏈表Adlist字典Dict跳躍表,如果有對Redis常見的類型或底層數據結構不明白的請看上面傳送門。

今天來說下set的底層實現整數集合,如果有對set不明白的,常見的API使用這篇就不講了,看上面的傳送門哈。

整數集合概念

整數集合是Redis設計的一種底層結構,是set的底層實現,當集合中只包含整數值元素,並且這個集合元素數據不多時,會使用這種結構。但是如果不滿足剛才的條件,會使用其他結構,這邊暫時不講哈。

下圖為整數集合的實際組成,包括三個部分,分別是編碼格式encoding,包含元素數量length,保存元素的數組contents。(這邊只需要簡單看下,下面針對每個模塊詳細說明哈)

整數集合的實現

我們看下intset.h裏面關於整數集合的定義,上代碼哈:

//整數集合結構體
typedef struct intset {
    uint32_t encoding;  //編碼格式,有如下三種格式,初始值默認為INTSET_ENC_INT16
    uint32_t length;    //集合元素數量
    int8_t contents[];  //保存元素的數組,元素類型並不一定是ini8_t類型,柔性數組不佔intset結構體大小,並且數組中的元素從小到大排列。
} intset;               

#define INTSET_ENC_INT16 (sizeof(int16_t))   //16位,2個字節,表示範圍-32,768~32,767
#define INTSET_ENC_INT32 (sizeof(int32_t))   //32位,4個字節,表示範圍-2,147,483,648~2,147,483,647
#define INTSET_ENC_INT64 (sizeof(int64_t))   //64位,8個字節,表示範圍-9,223,372,036,854,775,808~9,223,372,036,854,775,807

 

 

編碼格式encoding

包括INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64三種類型,其分別對應着不同的範圍,具體看上面代碼的註釋信息。

因為插入的數據的大小是不一樣的,為了盡可能的節約內存(畢竟都是錢,平時要省着點用),所以我們需要使用不同的類型來存儲數據。

集合元素數量length

記錄了保存數據contents的長度,即有多少個元素。

保存元素的數組contents

真正存儲數據的地方,數組是按照從小到大有序排序的,並且不包含任何重複項(因為set是不含重複項,所以其底層實現也是不含包含項的)。

整數集合升級過程(重點,手動標星)

上面的圖我們重新看下,編碼格式encoding為INTSET_ENC_INT16,即每個數據佔16位。長度length為4,即數組content裏面有四個元素,分別是1,2,3,4。如果我們要添加一個数字位40000,很明顯超過編碼格式為INTSET_ENC_INT16的範圍-32,768~32,767,應該是編碼格式為INTSET_ENC_INT32。那麼他是如何升級的呢,從INTSET_ENC_INT16升級到INTSET_ENC_INT32的呢?

1.了解舊的存儲格式

首先我們看下1,2,3,4這四個元素是如何存儲的。首先要知道一共有多少位,計算規則為length*編碼格式的位數,即4*16=64。所以每個元素佔用了16位。

2.確定新的編碼格式

新的元素為40000,已經超過了INTSET_ENC_INT16的範圍-32,768~32,767,所以新的編碼格式為INTSET_ENC_INT32。

3.根據新的編碼格式新增內存

上面已經說明了編碼格式為INTSET_ENC_INT32,計算規則為length*編碼格式的位數,即5*32=160。所以新增的位數為64-159。

4.根據編碼格式設置對應的值

從上面知道按照新的編碼格式,每個數據應該佔用32位,但是舊的編碼格式,每個數據佔用16位。所以我們從後面開始,每次獲取32位用來存儲數據。

這樣說太難懂了,看下圖。

首先,那最後32位,即128-159存儲40000。那麼第49-127是空着的。

接着,取空着的49-127最後的32位,即96到127這32位,用來存儲4。那麼之前4存儲的位置48-6349-127剩下的64-95這兩部分組成了一個大部分,即48-95,現在空着啦。

在接着在48-95這個大部分,再取后32位,即64-95,用來存儲3。那麼之前3存儲位置32-4748-95剩下的48-63這兩部分組成了一個大部分,即32-63,現在空着啦。

再接着,將32-63這個大部分,再取后32位,即還是32-63,用來存儲2。那麼之前2存儲位置16-31空着啦。

最後,將16-31和原來0-31合起來,存儲1。

至此,整個升級過程結束。整體來說,分為3步,確定新的編碼格式,新增需要的內存空間,從后往前調整數據。

這邊有個小問題,為啥要從后往前調整數據呢?

原因是如果從前往後,數據可能會覆蓋。也拿上面個例子來說,數據1在0-15位,數據2在16-31位,如果從前往後,我們知道新的編碼格式INTSET_ENC_INT32要求每個元素佔用32位,那麼數據1應該佔用0-31,這個時候數據2就被覆蓋了,以後就不知道數據2啦。

但是從后往前,因為後面新增了一些內存,所以不會發生覆蓋現象。

升級的優點

 節約內存

整數集合既可以讓集合保存三種不同類型的值,又可以確保升級操作只在有需要的時候進行,這樣就節省了內存。 

不支持降級

一旦對數組進行升級,編碼就會一直保存升級后的狀態。即使後面把40000刪掉了,編碼格式還是不會將會INTSET_ENC_INT16。

整數集合的源碼分析

創建一個空集合 intsetnew

這個方法比較簡單,是初始化整數集合的步驟,即下圖部分。

主要的步驟是分配內存空間,設置默認編碼格式,以及初始化數組長度length。

intset *intsetNew(void) {
    intset *is = zmalloc(sizeof(intset));//分配內存空間 
    is->encoding = intrev32ifbe(INTSET_ENC_INT16);//設置默認編碼格式INTSET_ENC_INT16 
    is->length = 0;//初始化length 
    return is;
}

添加元素並升級insetAdd流程圖(重點)

添加元素並升級insetAdd源碼分析

可以根據上面的流程圖,對照着下面的源碼分析,這邊就不寫啦哈。

//添加元素
//輸入參數*is為原整數集合
//value為要添加的元素
//*success為是否添加成功的標誌量 ,1表示成功,0表示失敗 
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    //確定要添加的元素的編碼格式 
    uint8_t valenc = _intsetValueEncoding(value);
    
    uint32_t pos;
    //如果success沒有初始值,則初始化為1 
    if (success) *success = 1;

   //如果新的編碼格式大於現在的編碼格式,則升級並添加元素 
    if (valenc > intrev32ifbe(is->encoding)) {
        //調用另一個方法 
        return intsetUpgradeAndAdd(is,value);
    } else {
        //如果編碼格式不變,則調用查詢方法 
        //輸入參數is為原整數集合 
        //value為要添加的數據
        //pos為位置 
        if (intsetSearch(is,value,&pos)) {//如果找到了,則直接返回,因為數據是不可重複的。 
            if (success) *success = 0;
            return is;
        }

        //設置length 
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    //設置數據 
    _intsetSet(is,pos,value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}


//#define INT8_MAX 127
//#define INT16_MAX 32767
//#define INT32_MAX 2147483647
//#define INT64_MAX 9223372036854775807LL 
static uint8_t _intsetValueEncoding(int64_t v) {
    if (v < INT32_MIN || v > INT32_MAX)
        return INTSET_ENC_INT64;
    else if (v < INT16_MIN || v > INT16_MAX)
        return INTSET_ENC_INT32;
    else
        return INTSET_ENC_INT16;
}


//根據輸入參數value的編碼格式,對整數集合is的編碼格式升級 
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    //當前集合的編碼格式 
    uint8_t curenc = intrev32ifbe(is->encoding);
    //根據對value解析獲取新的編碼格式 
    uint8_t newenc = _intsetValueEncoding(value);
    //獲取集合元素數量 
    int length = intrev32ifbe(is->length);
    //如果要添加的數據小於0,則prepend為1,否則為0 
    int prepend = value < 0 ? 1 : 0;

   //設置集合為新的編碼格式,並根據編碼格式重新設置內存 
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    //逐步循環,直到length小於0,挨個重新設置每個值,從后往前 
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    //如果value為負數,則放在最前面 
    if (prepend)
        _intsetSet(is,0,value);
    else//如果value為整數,設置最末尾的元素為value 
        _intsetSet(is,intrev32ifbe(is->length),value);
    //重新設置length 
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}


//找到is集合中值為value的下標,返回1,並保存在pos中,沒有找到返回0,並將pos設置為value可以插入到數組的位置
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
    int64_t cur = -1;

    //如果集合為空,那麼位置pos為0 
    if (intrev32ifbe(is->length) == 0) { 
        if (pos) *pos = 0;
        return 0;
    } else {
        //因為數據是有序集合,如果要添加的數據大於最後一個数字,那麼直接把要添加的值放在最後即可,返回最大值下標 
        if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
            if (pos) *pos = intrev32ifbe(is->length);
            return 0;
        } else if (value < _intsetGet(is,0)) { //如果這個數據小於數組下標為0的數據,即為最小值 ,返回0 
            if (pos) *pos = 0;
            return 0;
        }
    }
    //有序集合採用二分法 
    while(max >= min) {
        mid = ((unsigned int)min + (unsigned int)max) >> 1;
        cur = _intsetGet(is,mid);
        if (value > cur) {
            min = mid+1;
        } else if (value < cur) {
            max = mid-1;
        } else {
            break;
        }
    }

    //確定找到 
    if (value == cur) {
        if (pos) *pos = mid;//設置參數pos,返回1,即找到位置 
        return 1;
    } else {//如果沒找到,則min和max相鄰,隨便設置都行,並返回0 
        if (pos) *pos = min; 
        return 0;
    }
}

 

結語

該篇主要講了Redis的SET數據類型的底層實現整數集合,先從整數集合是什麼,,剖析了其主要組成部分,進而通過多幅過程圖解釋了intset是如何升級的,最後結合源碼對整數集合進行描述,如創建過程,升級過程,中間穿插例子和過程圖。

如果覺得寫得還行,麻煩給個贊,您的認可才是我寫作的動力!

如果覺得有說的不對的地方,歡迎評論指出。

好了,拜拜咯。

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

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

印尼蘇門答臘象身首異處慘死 象牙遭盜採

摘錄自2019年11月8日自由時報屏東報導

印尼保育官員今天表示,1頭列為極危(critically endangered)物種的蘇門答臘象屍體被發現,牠的頭被砍下且象牙被拔走,顯然是一宗盜獵案件。

蘇門答臘島廖內省(Riau)一名農園工人昨天發現這頭40歲公象腐爛的屍體。當地保育機關主管蘇哈尤諾(Suharyono)在聲明中說:「大象的頭被砍下,切斷的象鼻落在距離象身1公尺處。」當局正在追查犯案人士。

森林濫伐造成蘇門答臘象的天然棲地縮減,導致牠們和人類的衝突加劇。另一方面,蘇門答臘象象牙在野生動物黑市交易中價值連城。

去年在印尼亞齊省(Aceh)也發現一具顯然被毒死的蘇門答臘象屍體,當時牠的象牙也不見。印尼環境部估計,境內野生蘇門答臘象只剩不到2000頭。

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

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

無視巴黎氣候協定 全球化石燃料產量遠高於限制

摘錄自2019年11月20日中央通訊社報導

聯合國和頂尖研究團體今天(20日)發布報告警告,全球已規劃或準備進行的石油、天然氣和煤炭產量,將遠遠超越抑制全球暖化讓地球維持適合人居所需的產量目標。

聯合國環境規劃署(UN Environment Programme)和4個氣候變遷研究中心聯合發布報告指出,全球預計生產的化石燃料總量,較為了讓地表溫度較工業革命前水準升高不超過攝氏2度所容許的燃燒量,超出達50%。

若將地表升溫幅度限制在攝氏1.5度,則計劃中的化石燃料產量將較容許數量超出1倍多。2015年達成的巴黎氣候協定,要求將全球暖化限制在遠低於攝氏2度水準,可能的話僅升溫攝氏1.5度。

儘管截至目前全球僅升溫攝氏1度,但全世界已出現逐漸增強的致命熱浪、洪災和超級風暴,而超級風暴因海平面加速上升而破壞力更強大。研究人員警告,煤炭、石油和天然氣供應的「過度投資」,與未來數十年必須大幅縮減溫室氣體排放的目標,兩者直接相衝突。

聯合國去年發布的報告斷定,若要抑制地表升溫僅攝氏1.5度,則全球二氧化碳排放必須在2030年底前減少45%,並於2050年底前達到「淨零排放」。

斯德哥爾摩環境研究所(Stockholm Environment Institute)美國中心主任賴薩魯斯(Michael Lazarus)表示:「我們首次展現,巴黎(氣候協定的)溫度目標,和各國煤炭、石油和天然氣的生產計畫及政策,兩者間落差有多巨大。」

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

敢反抗開發就坐牢 原民反土地改革惡法 印尼動亂中媒體忽略的訴求

環境資訊中心外電;姜唯 翻譯;林大利 審校;稿源:Mongabay

9月下旬,印尼史上繼1998年獨裁者蘇哈托倒台以來最大規模的抗爭行動,佔據國際新聞版面。英國、美國]、和其他外國媒體頭條報導成千上萬民眾在全國各地大城市示威遊行,抗議婚前性行為納入刑法。

撤銷新刑法法條,包括不得侮辱總統和提供避孕資訊,只是抗議者的訴求之一。根據網上流傳的七點聲明,訴求還包括反對廢除一條弱化反貪腐機構的新法、停止蘇門答臘和婆羅洲的森林大火,以及從印尼東邊的巴布亞地區撤軍。印尼對分裂主義的軍事鎮壓行動已經進行了數十年。

撤銷土地利用新法也在訴求清單上。

這條尚未被媒體大幅報導,但觀察者表示,它的重要性不亞於其他幾乎立法完成的爭議性法案。

評論家說,該法案定義了新的罪行,並增加了刑罰,使當局更容易監禁抵抗開發商入侵的農村居民,使農業公司能保留土地特許經營權更長的時間。

印尼巴布亞省一條切開森林的公路,涉嫌剝奪了原住民的土地所有權。Nanang Sujana攝;來源:Mongabay。

在批評者眼中,最令人髮指的是,該法案設定了一個兩年期限,要求公民必須在期限內向政府註冊土地,否則土地將收歸國有,成為佐科威總統土地改革計畫的一部分重新分配,或授權私人公司使用。

但是,原住民尋求正式承認其土地就起碼要花兩年,通常還要更久,才能通過層層官僚關卡。因此,兩年期限對印尼弱勢的原住民權利來說更是一記沈重的打擊,印尼原住民組織「AMAN」副秘書長卡亞迪(Erasmus Cahyadi)說。

2013年印尼憲法法院作出歷史性裁決,駁回了州政府對原住民森林所有權的主張,此後總統佐科威陸續承認55個原住民群體對森林的權利,總面積達248平方公里(96平方英里)。但是AMAN說他們欲承認的原民土地有77,600平方公里(30,000平方英里),屬於704個原住民族。

AMAN法律與人權事務負責人穆罕默德(Arman Muhammad)說,該法案違反了憲法的精神。

印尼大學生走上雅加達街頭,反對新法律弱化反貪腐機構。Hans Nicholas Jong攝;來源:Mongabay。

該法案的支持者則認為,通過該法案對於佐科威的大型土地改革計畫來說是必要的。

佐科威於今年4月當選第二個五年任期,他已承諾賦予農村社區更大的權力,控制其21.7萬平方公里(84,000平方英里)的土地,但是實踐進度緩慢。

截至10月,控制該國約一半土地的環境和林業部僅分配了總計28,000平方公里的土地,遠低於目標127,000平方公里。

民主黨國會議員卡隆(Herman Khaeron):「土地改革計畫的土地很難找。」

為了解決這個問題,卡隆說,該法案要求建立一個新的機構,稱為土地管理署,負責收購、管理和分配兩年的時間內沒有被公民登記的土地,這些土地將自動收歸國有。

根據該法案,土地管理署將充當「土地銀行」,能夠透過租賃或出售土地產生收入,同時仍以非營利組織的形式運作。該機構必須保證為「社會利益」和「發展利益」提供土地。

法案的措詞含糊,批評者擔心該機構會將土地當作商品出售給強勢投資者,以犧牲普通市民為代價。

「誰將能夠使用這個土地銀行?小農嗎?當然不是,」印尼茂物農業研究所(Bogor Institute of Agriculture, IPB)人類生態學系研究員卡友諾(Eko Cahyono)說,「這個土地銀行將為有大量資本、企業和開發計畫的人服務。」

評論家說,該法案中的其他條款將使企業受益,傷害農村社區。

該法案將允許農業公司持有「HGU」耕種權許可證,有效期長達90年,而現行規定為60年。油棕公司可以拖更久的時間才將小農地釋出給當地社區。

此外,該法條還規定,凡訂定「引起土地糾紛之惡意協議」者,將被判處5至15年監禁,「妨礙土地機關僱員和/或執法人員執行任務」者可判處兩年監禁。

倡議組織土地改革聯盟(Consortium for Agrarian Reform, KPA)秘書長卡蒂卡(Dewi Kartika)向記者,根據後項條款,反抗土地掠奪的原住民、行動人士等人可被入罪。

「它賦予警察將任何人定罪的合法性。最極端的詮釋之下,可被用來逮捕任何人。例如,當居民試圖阻止他們的土地被用來建造機場時,就可能會被逮捕。」

成百上千的印尼村莊在和農業公司對抗時陷入困境,社區成員常常以肉身擋推土機,甚至縱火燒毀公司設備。

9月26日,一名21歲的大學生在蘇拉威西省東南部省會肯達里的大規模抗議活動中被警察槍殺。抗議活動演變成暴亂後,另一名19歲學生卡德哈維(Yusuf Qardhawi)因鈍器重創頭部而死亡。

「我們非常沮喪和失望,」參加抗議活動的社區組織者、23歲的馬斯庫里(Mando Maskuri)說,「國家應該保護人民,但他們卻在殺死人民。」

。與印尼其他地方一樣,當地人往往缺乏證明土地所有權的文件,這使得州政府很容易在未經他們同意的情況下引入企業投資者。

馬斯庫里說,許多旺尼居民嘗試向州政府登記其土地。但他擔心土地法案設定的時間表不切實際,最終導致居民失去土地,逼他們搬走。

大學生在肯達里用行動劇抗議礦場開發。Kamaruddin攝,來源:Mongabay

在9月抗議活動的高峰期,對土地法案和其他爭議性法案的審議被。跛鴨議會會期即將結束,新的議員宣誓就職。

現任議員在最後一刻同意將土地法案移交至新議會會期,這表示新任議員可以從同一階段繼續審議,不必從頭開始。

根據調查性新聞機構Tempo和非政府組織Auriga Nusantara的,接下來五年負責立法工作的575名議員中,將近一半是與至少1,016家企業有聯繫的商人,其中包括採礦公司和油棕公司。

佐科威總統說,他想,使之對投資者更加友善。許多觀察家表示,不少與已列入清單。

研究人員卡友諾說,如果議員試圖通過土地法案,反對者可以在最後一道關卡提出司法審查。

同時,馬斯庫里說他準備再次走上街頭,「如果議會要強行通過該法案,將面臨來自農民、漁民和民間社會團體的巨大阻力。」

Indonesia protests: Land bill at center of unrest by Basten Gokkon, Hans Nicholas Jong, Philip Jacobson on 3 November 2019

  • In recent weeks, Indonesia has seen its largest mass protests since the “people power” movement that forced President Suharto to step down in 1998.
  • Among a variety of pro-democracy demands, the protesters want lawmakers to scrap a controversial bill governing land use in the country.
  • The bill defines new crimes critics say could be used to imprison indigenous and other rural citizens for defending their lands against incursions by private companies.
  • It also sets a two-year deadline by which citizens must register their lands with the government, or else watch them pass into state control. Activists say the provision would deal a “knockout blow” to the nation’s indigenous rights movement.

JAKARTA — In late September, international news outlets caught flak for their coverage of Indonesia’s largest mass protests since the 1998 uprising that led to the fall of the dictator Suharto.

Headlines published by the , , and other foreign media implied the demonstrations, involving tens of thousands of people in major cities across the country, had arisen in response to a proposed new criminal code that would ban sex before marriage.

“I did not get tear-gassed so Australians could keep having sex in Bali,” one netizen on Twitter, among a barrage of reactions to the reductive reports. “This is about the future of the country.”

Scrapping the criminal code changes — which also include new penalties for insulting the president and providing information about contraception — was just one of the protesters’ demands, enumerated in a seven-point declaration that has circulated online. They also want the government to repeal a new law weakening the nation’s anti-corruption agency, stop forest fires in Sumatra and Borneo, and withdraw troops from Indonesia’s easternmost Papua region, where a military crackdown against separatists has been going on for decades.

Also on the list: scrap a proposed new law governing land use.

Though the land bill has gotten scant media coverage, observers say it is among the most potentially transformative of a raft of on the verge of being passed into law.

The bill defines new crimes and introduces increased penalties that, critics say, would make it easier for authorities to imprison rural citizens for defending their lands against incursions by developers. It would also allow plantation companies to retain vast land concessions for longer periods of time.

Most damningly in the eyes of critics, the bill sets a two-year deadline by which citizens must register their lands with the government, or else watch them pass into state control, where they could be redistributed as part of President Joko Widodo’s land reform program or licensed out to private firms.

But indigenous groups seeking formal recognition of their lands already spend at least that long, and often far longer, jumping through bureaucratic hoops. The two-year deadline would therefore constitute a “knockout blow” for the nation’s embattled indigenous rights movement, Erasmus Cahyadi, deputy secretary-general of AMAN, Indonesia’s main advocacy group for indigenous peoples, told Mongabay.

Since 2013, when a landmark Constitutional Court ruling struck down the state’s claim to indigenous peoples’ forests, President Joko Widodo has recognized the rights of 55 indigenous groups to forests spanning a total of 248 square kilometers (96 square miles). But AMAN says it has mapped more than 77,600 square kilometers (30,000 square miles) of land it says belongs to 704 indigenous communities.

“The bill is contrary to the spirit of the constitution,” said Arman Muhammad, AMAN’s law and human rights director.



University students protest the new corruption law in Jakarta. Image by Hans Nicholas Jong/Mongabay.

The bill’s supporters argue its passage is necessary to support President Widodo’s flagship land reform program.

Widodo, who was elected to a second five-year term in April, has promised to give rural communities greater control over 217,000 square kilometers (84,000 square miles) of land. But progress has been slow.

As of October, the Ministry of Environment and Forestry, which controls around half of the nation’s land, had only distributed a total of 28,000 square kilometers (10,800 square miles), far short of its target of 127,000 square kilometers (49,000 square miles).

“It’s hard to find land for the agrarian reform [program],” Democrat Party lawmaker Herman Khaeron at a recent panel event in Jakarta.

To solve that, Herman said, the bill calls for the creation of a new body called the Land Management Agency to acquire, manage and distribute land that had gone unclaimed by citizens during the two-year window, that therefore automatically fell under state control.

The bill says the agency will function as a “land bank,” implying it will be able to generate an income from leasing or selling lands, while still operating as a “nonprofit,” according to the bill. The agency must guarantee the availability of land for “social interests” as well as “development interests.”

The language in the bill is vague, but critics fear the agency would treat land as a commodity to be sold to powerful investors at the expense of ordinary citizens.

“Who would be able to access this land bank? Small farmers? Of course not,” Eko Cahyono, a researcher in the Department of Human Ecology at the Bogor Institute of Agriculture (IPB), told Mongabay. “The ‘land bank’ would serve those with big capital, companies and development projects.”

Other provisions in the bill would benefit corporations at the expense of rural communities, critics say.

The bill would allow plantation companies to hold a right-to-cultivate permit, known as an HGU, for 90 years, up from 60 years under the current rules.

It would also let oil palm firms wait longer before providing smallholdings to local communities, a requirement under existing laws.

Furthermore, the legislation stipulates prison time of five to 15 years for anyone who makes an “evil agreement that gives rise to a land dispute,” and a jail term of two years for those who “obstruct an employee and/or law enforcement officer from carrying out tasks in the land sector.”

The latter provision could be used to “criminalize indigenous peoples, activists or anyone who tries to organize” against a land grab, Dewi Kartika, the secretary-general of the Consortium for Agrarian Reform (KPA), an advocacy group, reporters in Jakarta recently.

“It grants the police legal legitimacy to criminalize anyone,” she said. “Of course this will be interpreted to the maximum extent possible, to freely arrest anyone. For example, if residents try to stand in the way of their land being used to build an airport.”

Hundreds, if not thousands, of Indonesian villages are embroiled in conflict with natural resources firm, with community members often resorting to physically blocking bulldozers or even setting fire to company facilities.

On Sept. 26, a 21-year-old college student in Kendari, the capital of Southeast Sulawesi province and one of the cities where mass protests took place in September, was shot dead by police. Another student in Kendari, 19-year-old Yusuf Qardhawi, died of blunt-force head injuries after a protest turned into a violent riot.

“We were all so upset and disappointed,” Mando Maskuri, 23, a community organizer who joined the protests in Kendari, told Mongabay. “The state is supposed to protect us, but they’re killing us.”

Residents of Mando’s home island of Wawonii are with mining firms that hold permits to operate on their lands. As elsewhere in Indonesia, locals tend to lack documents backing their land claims, making it easy for the state to bring in corporate investors without their consent.

Many people in Wawonii are trying to register their lands with the state, Mando said. But he fears the land bill sets an unrealistic timeline that will eventually cause residents to lose their lands, forcing them to migrate to other parts of the country.



Students in Kendari stage a mock burial in early 2019 to express their opposition to the mining in Wawonii. Image by Kamaruddin for Mongabay.

At the height of the protests in September, deliberations on the land bill and other controversial legislation were . The lame-duck parliament was nearing the end of its session. New lawmakers have since been sworn in.

In their final hour, however, the previous lawmakers agreed to “carry over” the land bill to the current parliament session, meaning deliberations can be resumed from the same stage by the new batch of legislators, rather than having to start all over again.

Nearly half of the 575 lawmakers for the next five years are businesspeople who are affiliated with at least 1,016 companies, including mining and oil palm, according to an by investigative journalism outlet Tempo and Auriga Nusantara, an NGO.

President Widodo says he wants to to make them friendlier to investors; many observers have said are on the list.

If lawmakers try to pass the land bill, opponents could file a judicial review in a last-ditch attempt to oppose it, said Eko, the researcher.

In the meantime, Mando says he is ready to take to the streets again.

“If parliament tries to pass the bill, there will be massive resistance from farmers, fishermen, and civil society groups,” he said.

※ 全文及圖片詳見:

作者

如果有一件事是重要的,如果能為孩子實現一個願望,那就是人類與大自然和諧共存。

於特有生物研究保育中心服務,小鳥和棲地是主要的研究對象。是龜毛的讀者,認為龜毛是探索世界的美德。

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

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務