Skip to content

MicroStream Persistence Adapter

Jan Wiemer edited this page Jun 16, 2022 · 8 revisions

The MicroStream Persistence Adapter

One implementation of the persistence adapter is already shipped with the JACIS project, the MicrostreamPersistenceAdapter. This extension uses the MicroStream framework to serialize the committed objects to the file system (or a different persistent storage). The advantage of the MicroStream serialization is that it serializes the Java objects (stored in the JACIS) as they are. There is no need to specify any metadata for any mapping to a persistent data structure (like object-relational mapping required e.g. for the Java Persistence API (JPA)). Another important feature, distinguishing it from the normal Java serialization, is that after modifications not the whole object graph, but only the changed objects have to be written again. More details regarding the features of the MicroStream framework con be found in the MicroStream User Manual.

Concept of the MicroStream Persistence Adapter

Internally in JACIS the committed objects are stored in a java.util.HashMap. The objects are independent and self contained, therefore following the references of one entry we can never reach another one. In a typical application we have a huge amount of objects and quite small but very frequent transactions. Usually a single transaction only modifies a few objects. The goal is to keep the costs to store the modification of one object as well as the addition and deletion of objects constant. This means the time needed to update, insert or delete an object must not depend on the number of stored objects. Since storing a standard Java map or list in MicroStream does currently not guarantee constant time for adding or deleting objects the stored objects are organized in a specialized data-structure.

The MicroStreamPersistenceAdapter

JACIS Extension Microstream

The stored objects are stored in a doubly linked list. Therefore the objects are wrapped by a class (MicrostreamStoreEntity) also containing a link to the predecessor and the successor in the list. New objects are added at the head of the list (obviously requiring only constant time). For deleted objects the references stored in the predecessor and successor objects have to be adapted. This is also possible in constant time. For the store a root object (class: MicrostreamStoreRoot) simply points to the first element in the list.

Each time an object in the JACIS is modified we have to find and update the wrapped objects in the linked list. This object has to be updated in the MicroStream storage (e.g. by calling the store on the one.microstream.storage.types.StorageManager). Therefore the MicroStream persistence adapter stores the wrapped object (the linked list entry, class: MicrostreamStoreEntity) for each store-key in a map (key2entry). During the commit phase of a transaction the modified entries are cloned back to the store. For each modified object the onModification method of the MicrostreamPersistenceAdapter is called. In this method the linked list is adapted and all modified list entries (also the wrapped objects where only the links to the successor or predecessor have been changed) are stored in a set. Finally after the commit is finished the objects in this set are passed to the storeAll method of the MicroStream storage manager to persist the changes.

Structure of the MicroStream Persistence Adapter

The MicroStream Persistence Adapter has two main components: the persistence adapter itself (class MicrostreamPersistenceAdapter) and a class representing the MicroStream storage (class MicrostreamStorage). For each store there exists an instance of the persistence adapter tracking the modifications for this store and maintaining the linked list of store entries. The MicroStream storage wrapps the access to the MicroStream API itself. Each instance of the MicroStream storage contains a MicroStream storage manager providing access to a persistence store. The persistence adapter is designed in a way that it is possible to use a single MicroStream storage manager for multiple MicroStream Persistence Adapter instances of multiple stores. The store root objects (MicrostreamStoreRoot) maintained by the persistent adapter instances are all referenced in a map by a single MicroStream root object (MicrostreamRoot). This way it is possible to store the modified objects from all stored after the global commit in a single atomic call of the storeAll method.

Using the MicroStream Persistence Adapter

To use the MicroStream persistence adapter we first have to include the needed dependencies to our application (here in gradle format):

dependencies {
    implementation group: 'org.jacis', name: 'jacis', version: '2.0.0-SNAPSHOT'
    implementation group: 'one.microstream', name: 'storage.embedded', version: '06.01.00-MS-GA'
    implementation group: 'one.microstream', name: 'storage.embedded.configuration', version: '06.01.00-MS-GA'
}

With this the following example shows how to use the persistence adapter:

  public static void main(String[] args) {
    { // first start a container and a store with persistence
      JacisContainer container = new JacisContainer();
      JacisObjectTypeSpec<String, Account, Account> objectTypeSpec //
          = new JacisObjectTypeSpec<>(String.class, Account.class, new JacisCloningObjectAdapter<>());
      // start a MicroStream storage manager
      EmbeddedStorageManager storageManager = createMicroStreamStorageManager();
      // create MicroStream storage:
      MicrostreamStorage storage = new MicrostreamStorage(storageManager);
      // create and set the persistence adapter extension
      objectTypeSpec.setPersistenceAdapter(new MicrostreamPersistenceAdapter<>(storage));
      JacisStore<String, Account> store = container.createStore(objectTypeSpec).getStore();
      // create some objects
      container.withLocalTx(() -> {
        store.update("account1", new Account("account1").deposit(-100));
        store.update("account2", new Account("account2").deposit(10));
        store.update("account3", new Account("account3").deposit(100));
      });
      storageManager.close();
    }
    { // simulate restart and start a new container and a new store
      JacisContainer container = new JacisContainer();
      JacisObjectTypeSpec<String, Account, Account> objectTypeSpec //
          = new JacisObjectTypeSpec<>(String.class, Account.class, new JacisCloningObjectAdapter<>());
      // start a MicroStream storage manager
      EmbeddedStorageManager storageManager = createMicroStreamStorageManager();
      // create MicroStream storage:
      MicrostreamStorage storage = new MicrostreamStorage(storageManager);
      // create and set the persistence adapter extension
      objectTypeSpec.setPersistenceAdapter(new MicrostreamPersistenceAdapter<>(storage));
      JacisStore<String, Account> store = container.createStore(objectTypeSpec).getStore();
      // check the objects are still in the store
      container.withLocalTx(() -> {
        store.stream().forEach(acc -> System.out.println("balance(" + acc.getName() + ")= " + acc.getBalance()));
      });
      storageManager.close();
    }
    System.exit(1);
  }

  protected static EmbeddedStorageManager createMicroStreamStorageManager() {
    EmbeddedStorageManager storageManager = EmbeddedStorageConfigurationBuilder.New() //
        .setStorageDirectory("var/data-dir") //
        .setBackupDirectory("var/backup-dir") //
        .createEmbeddedStorageFoundation() //
        .createEmbeddedStorageManager();
    return storageManager;
  }

It is also possible to read the MicroStream configuration from a configuration file:

  protected static EmbeddedStorageManager createMicroStreamStorageManager() {
    EmbeddedStorageManager storageManager = EmbeddedStorageConfiguration.load("microstream.ini") //
        .createEmbeddedStorageFoundation() //
        .createEmbeddedStorageManager();
    return storageManager;
  }

In this case e.g. the directories can be can be configured in the "microstream.ini" file (located in the classpath, e.g. in "src/main/resources"):

# file microstream.ini
baseDirectory = var/data-dir
backupDirectory = var/backup-dir

Next Chapter: Transaction Listener