DDL-臟數據層的實現

  • 2019 年 11 月 3 日
  • 筆記

在我們的項目中,經常會有一些數據會涉及到頻繁更改。如果每次都從資料庫中讀取再修改,這樣不僅浪費時間,而且還更加危險。那此時我們究竟該如何解決這個問題呢?此時,DDL(臟數據層)就出現了。

首先說一下為什麼操作不能保證原子性就會危險,因為這時就很有可能出現同時修改的情況,最終的結果極有可能並不是你所希望的(除非這些操作都是冪等性,但這種情況應該比較少)。如果是利用資料庫中的鎖,一來我在項目中用的比較少,二來也增加了維護難度。當然,有人說可以利用CAS,那針對一些複雜的情況(比如類裡面屬性的修改會有一些相關性,你的一次更改需要涉及幾個屬性等),可能你還是需要單獨設計一套系統,而且還會有經典的ABA問題。如果你是利用CAS解決的,希望能夠在下方評論區告知,就當互相學習。

那現在來說說DDL層具體是什麼。DDL全稱是Dirty Data Layer,即臟數據層。針對那些在系統運行經常會更改的domain類,我們將其再做一次封裝,組成一個類似map的形式。單獨由一組執行緒來管理這些map,每當有數據改動時,我們就往這個map中添加內容,而我們的執行緒則定期向資料庫中寫入內容。這樣做的好處,首先是讓你的每一次操作都沒有IO的參與,提高了相應速度,而且定時提交意味著你可以把原本的幾次提交變成為1次,減少了和資料庫的交互。當然,缺點也是存在的,如果你的系統是分散式,那麼你的這個DDL層的實現可能就沒有那麼方便,因為這些數據你可能需要存儲在類似Redis這種共享快取中,因此每次的拿和取就需要封裝一下(這個應該算是小問題,因為原本就算你用的是本地快取,所有操作依舊是需要封裝的,只不過你的IO消耗由原本的資料庫變成了共享快取)。接下來,我就針對本地快取的情況來具體實現一個DDL。

定義操作

這是我定義出的一些操作:

public interface IDirtyEntity {      //region manage content      /**     * 獲取entity的內容。     */    Object getContent();      /**     * 獲取entity的內容。 獲取的內容是複製的對象,屬性值是調用該方法時的屬性值。     */    Object copyContent();      //endregion      //region persisting flag      /**     * 是否正在進行持久化     */    boolean isPersisting();      /**     * 設置正在持久化標誌     */    void setPersistingFlag();      /**     * 清除正在持久化標誌     */    void clearPersistingFlag();      //endregion      //region persist state      /**     * 設置為臟數據狀態     */    void setDirtyState();      /**     * 清除臟數據狀態     */    void clearDirtyState();      /**     * 當前持久化狀態。     *     * @see PersistState     */    PersistState currentPersistState();      //endregion      //region get/set field      /**     * 獲取屬性值。     */    Object getField(String fieldName);      /**     * 設置屬性值。     */    void setField(String fieldName, Object value);      /**     * 設置多個屬性的值。     */    void setFields(List<EntityField> fields);      /**     * 增加int類型屬性的值。     */    void addInt(String fieldName, int delta);      /**     * 增加long類型屬性的值。     */    void addLong(String fieldName, long delta);      //endregion      //region manage dirty field      /**     * 標記臟數據欄位     */    void addDirtyField(String fieldName);      /**     * 獲取修改過的屬性。     */    List<EntityField> getAndClearDirtyFields();      //endregion      //region wrapper implement      /**     * 返回id的屬性名。     */    String getIdFieldName();      /**     * 返回id     */    String getId();      /**     * 返回DATA的class     */    Class getDataClass();      //endregion  }

分類

DDL解決的是數據頻繁更改的問題,其實這裡的更改說的並不準確,並不僅僅只是update,還有insert。用過mongodb的應該清楚有一種叫upsert的操作,就是找到就修改,找不到就添加。我們這裡就需要將我們的數據分成兩類:Detachable(可拆分的)、Nondetachable(不可拆分的)。

可拆分的,就意味著你針對這個數據的修改最小可以精確到其中的一個屬性,項目中大多數都屬於這種情況。

不可拆分的,即每次都是以一個整體添加,比如一次交易,每次添加都是一個整體,不可能說你先提交買方,再提交賣方,後面還會修改買方。這種類型大多都是一條記錄,整體存入資料庫。

因此,我們來定義一下這兩種結構: 可拆分的類型:

import com.google.common.base.Preconditions;  import com.google.common.base.Strings;  import java.util.List;  import org.slf4j.Logger;  import org.slf4j.LoggerFactory;  import org.springframework.cglib.beans.BeanCopier;  import org.springframework.cglib.beans.BeanMap;  public abstract class DetachableDirtyEntityAdapter implements IDirtyEntity {      private static final Logger log = LoggerFactory.getLogger(DetachableDirtyEntityAdapter.class);      /**     * 數據屬性的map引用     */    private BeanMap beanMap;      private final BeanCopier beanCopier;        public DetachableDirtyEntityAdapter(Object content, BeanCopier beanCopier) {      Preconditions.checkNotNull(content);      Preconditions.checkNotNull(beanCopier);        this.content = newEmptyContentInstance();      this.beanCopier = beanCopier;      this.beanCopier.copy(content, this.content, null);      this.beanMap = BeanMap.create(this.content);    }      //region manage content      /**     * 數據的內容。     */    private Object content;      @Override    public Object getContent() {      return content;    }      private Object newEmptyContentInstance() {      Class cls = getDataClass();      try {        return cls.newInstance();      } catch (Exception e) {        log.error("initiate {} failed: {}", cls.getSimpleName(), e.getMessage());        return null;      }    }      @Override    public synchronized Object copyContent() {        Object copy = newEmptyContentInstance();      beanCopier.copy(this.content, copy, null);      return copy;    }      //endregion      //region persisting flag      private volatile boolean persisting = false;      @Override    public boolean isPersisting() {      return persisting;    }      @Override    public void setPersistingFlag() {      this.persisting = true;    }      @Override    public void clearPersistingFlag() {      this.persisting = false;    }      //endregion      //region persist state      @Override    public void setDirtyState() {      throw new UnsupportedOperationException();    }      @Override    public void clearDirtyState() {      throw new UnsupportedOperationException();    }      @Override    public synchronized PersistState currentPersistState() {      int dirtySize = dirtyFieldNames.size();        if (dirtySize == 0) {        return PersistState.PERSISTED;      } else {        return PersistState.DIRTY;      }    }      //endregion      //region get/set field      @Override    public synchronized Object getField(String fieldName) {      Preconditions.checkArgument(!Strings.isNullOrEmpty(fieldName));        return beanMap.get(fieldName);    }      @Override    public synchronized void setField(String fieldName, Object value) {      Preconditions.checkArgument(!Strings.isNullOrEmpty(fieldName));        beanMap.put(fieldName, value);      dirtyFieldNames.add(fieldName);    }      @Override    public synchronized void setFields(List<EntityField> fields) {      Preconditions.checkNotNull(fields);        for (EntityField f : fields) {        beanMap.put(f.getName(), f.getValue());        dirtyFieldNames.add(f.getName());      }    }      @Override    public synchronized void addInt(String fieldName, int delta) {      Preconditions.checkArgument(!Strings.isNullOrEmpty(fieldName));        int origin = (int) beanMap.get(fieldName);      beanMap.put(fieldName, origin + delta);      dirtyFieldNames.add(fieldName);    }      @Override    public synchronized void addLong(String fieldName, long delta) {      Preconditions.checkArgument(!Strings.isNullOrEmpty(fieldName));        long origin = (long) beanMap.get(fieldName);      beanMap.put(fieldName, origin + delta);      dirtyFieldNames.add(fieldName);    }      //endregion      //region manage dirty fields      /**     * 當前entity的包含臟數據的屬性名列表。     */      private final HashSet<String> dirtyFieldNames = new HashSet<>(16);      @Override    public void addDirtyField(String fieldName) {      Preconditions.checkArgument(!Strings.isNullOrEmpty(fieldName));      dirtyFieldNames.add(fieldName);    }      @Override    public synchronized List<EntityField> getAndClearDirtyFields() {      ArrayList<EntityField> list = new ArrayList<>();        for (String f : dirtyFieldNames) {        list.add(new EntityField(f, beanMap.get(f)));      }        // 清空dirtyFieldNames, 記錄上一次持久化的事件      dirtyFieldNames.clear();      return list;    }      //endregion  }

不可拆分的類型:

import com.google.common.base.Preconditions;  import com.google.common.base.Strings;  import java.util.ArrayList;  import java.util.HashSet;  import java.util.List;  import org.slf4j.Logger;  import org.slf4j.LoggerFactory;  import org.springframework.cglib.beans.BeanCopier;  import org.springframework.cglib.beans.BeanMap;  public abstract class NonDetachableDirtyEntityAdapter implements IDirtyEntity {      private static final Logger log = LoggerFactory.getLogger(NonDetachableDirtyEntityAdapter.class);      /**     * 數據屬性的map引用     */    private BeanMap beanMap;      private final BeanCopier beanCopier;        public NonDetachableDirtyEntityAdapter(Object content, BeanCopier beanCopier) {      Preconditions.checkNotNull(content);      Preconditions.checkNotNull(beanCopier);        this.content = newEmptyContentInstance();      this.beanCopier = beanCopier;      this.beanCopier.copy(content, this.content, null);      this.beanMap = BeanMap.create(this.content);    }      //region manage content      /**     * 數據的內容。     */    private Object content;      @Override    public Object getContent() {      return content;    }      private Object newEmptyContentInstance() {      Class cls = getDataClass();      try {        return cls.newInstance();      } catch (Exception e) {        log.error("initiate {} failed: {}", cls.getSimpleName(), e.getMessage());        return null;      }    }      @Override    public synchronized Object copyContent() {        Object copy = newEmptyContentInstance();      beanCopier.copy(this.content, copy, null);      return copy;    }      //endregion      //region persisting flag      private volatile boolean persisting = false;      @Override    public boolean isPersisting() {      return persisting;    }      @Override    public void setPersistingFlag() {      this.persisting = true;    }      @Override    public void clearPersistingFlag() {      this.persisting = false;    }      //endregion      //region persist state      private volatile PersistState persistState = PersistState.DIRTY;      @Override    public void setDirtyState() {      persistState = PersistState.DIRTY;    }      @Override    public void clearDirtyState() {      persistState = PersistState.PERSISTED;    }      @Override    public PersistState currentPersistState() {      return persistState;    }      //endregion      //region get/set field      @Override    public synchronized Object getField(String fieldName) {      Preconditions.checkArgument(!Strings.isNullOrEmpty(fieldName));        return beanMap.get(fieldName);    }      @Override    public synchronized void setField(String fieldName, Object value) {      Preconditions.checkArgument(!Strings.isNullOrEmpty(fieldName));        beanMap.put(fieldName, value);    }      @Override    public synchronized void setFields(List<EntityField> fields) {      Preconditions.checkNotNull(fields);        for (EntityField f : fields) {        beanMap.put(f.getName(), f.getValue());      }    }      @Override    public synchronized void addInt(String fieldName, int delta) {      Preconditions.checkArgument(!Strings.isNullOrEmpty(fieldName));        int origin = (int) beanMap.get(fieldName);      beanMap.put(fieldName, origin + delta);    }      @Override    public synchronized void addLong(String fieldName, long delta) {      Preconditions.checkArgument(!Strings.isNullOrEmpty(fieldName));        long origin = (long) beanMap.get(fieldName);      beanMap.put(fieldName, origin + delta);    }      //endregion      //region manage dirty fields    @Override    public void addDirtyField(String fieldName) {      throw new UnsupportedOperationException();    }      @Override    public synchronized List<EntityField> getAndClearDirtyFields() {      throw new UnsupportedOperationException();    }      //endregion  }

兩種類型最大的不同在於真正往資料庫中存儲時,前者是可以單獨欄位存儲,後者是整體存儲,因此最後和DirtyField相關的操作便需要注意,NondetachableDirtyEntityAdapter不需要記錄DirtyFields。

針對原本類中屬性的複製和存儲,我這兒用的是spring提供的BeanCopier,如果你有什麼更高效的工具,歡迎在下方留言。(我一直在找一種深度克隆高效的組件,試過kryo,但如果實現序列化介面,其效率和正常的set/get大概相差10倍,如果有好的組件,希望一併告知)。

以上就是DDL的準備工作,其實後面的工作就是將具體的類做一個封裝,再封裝針對該類的所有操作,然後另寫一個執行緒組執行往資料庫的寫入操作。這個工作其實針對各個項目都有其特殊的地方,部落客在這兒就不具體展示了,有興趣的話大家可以在下方留言。

Tags: