-
Notifications
You must be signed in to change notification settings - Fork 4
02. Snapshots and Iterators for n00bs
Snapshots are extremely powerful constructs that let you freeze a particular view for your read operations. Snapshots provide consistent read-only views for all the read operations. When reading values from database, it's quite possible other instances of DB
objects are making changes to database. In certain scenarios, you might want to freeze a snapshot of database at a particular point in time. This is where Snapshot
object will help you. Now since Snapshot
has to keep some additional data and resources while you are doing reads, it must be released once you are done with your operation. Creating and releasing snapshot is super simple:
using (var snapshot = db.GetSnapshot())
{
...
}
If you are not writing using
blocks in your code, be sure to call snapshot.Dispose()
.
Each ReadOption
object can take a Snapshot
property, and each read operation requires you to pass ReadOption
object. Once you have snapshot, you can use it as the following code illustrates:
using (var snapshot = db.GetSnapshot())
{
var val = db.Get(new ReadOptions { Snapshot = snapshot }, Slice.FromString("foo"));
}
Iterators of course are required when you want to iterate over the data stored in your key-value store. Now I won't go into details of comparators but by default (LevelDB
UWP) orders data by key bytes lexicographically. This behaviour can be customized with comparators, but it's out of scope for this article. So the best way to summarize the default key order is imagining my key value store has the following key value pairs inserted:
db.Put(Slice.FromString("b"), Slice.FromString("Two"))
db.Put(Slice.FromString("d"), Slice.FromString("Three"))
db.Put(Slice.FromString("a"), Slice.FromString("One"))
It would be stored in the following order: a => One, b => Two, d => Three
. This means iterating from beginning to end, you will encounter keys in order of a, b, c
since a < b < c
. Keeping this in mind, let's create an iterator that iterates over all key value pairs in database:
using (var itr = db.NewIterator(new ReadOptions()))
{
itr.SeekFirst();
while (itr.Valid())
{
byte[] key = itr.Key().ToByteArray();
byte[] val = itr.Value().ToByteArray();
itr.Next();
}
}
As you can see, just like anything else, an iterator must be disposed when you don't need it anymore. Iterator has methods like Seek
, SeekToFirst
, SeekToLast
, Valid
, Next
, and Prev
to navigate between your records (Checkout documentation). Additionally, it provides method Key
, and Value
to read the slices themselves.
Using Seek
,you can do powerful things like iterating over records with key X..Y
, for example:
using (var itr = db.NewIterator(new ReadOptions()))
{
itr.Seek(Slice.FromString("record001"));
while (itr.Valid())
{
var keySlice = itr.Key();
if (keySlice.ToString() == "record007") break;
// use keySlice
itr.Next();
}
}
The same can be done in reverse order:
using (var itr = db.NewIterator(new ReadOptions()))
{
itr.Seek(Slice.FromString("record007"));
while (itr.Valid())
{
var keySlice = itr.Key();
if (keySlice.ToString() == "record001") break;
// use keySlice
itr.Prev();
}
}
Just remember doing iteration in reverse order is an expensive operation and thus a little slower. Try using forward iterations whenever possible.
We can fuse iterators with snapshots to create really powerful features like getting set of key-value pairs at a particular instance in time while mutations are happening (something close to MVCC). It can't be any simpler:
using (var snapshot = db.NewSnapshot())
using (var itr = db.NewIterator(new ReadOptions({ Snapshot = snapshot })))
{
// use itr
}
Combining WriteBatches
and Iterators
with proper synchronization can yield powerful results. Imagination is your limit.
This completes the basic usage of LevelDB
. There is more advanced stuff that I will visit in future articles. You can look into the detailed documentation by going to LevelDB UWP Wiki.