LetsMakeIt was up until recently a beta Flash app that connected users to a shared whiteboard they could publish. Since Flash is dead, I figured I’d dig through some of the lessons learned in building such a beast. The first lesson is how to scale a service that allows near-unlimited User Generated Content (UGC). There’s lots of details around the margins like user searches, but here we’ll focus on supporting a simplified core API one might expect to implement in UGC using Redis data structures. Every API will run in sub-linear time (with some cheating), letting you scale to a well-populated Jupiter.
The API
create(c) // Make new content
read(id) // Get content by id
update(id, content) // Update content by id
delete(id) // Delete by content id
follow(uid) // Follow a user with id: uid, adding them to your feed
unfollow(uid) // Unfollow a user from your feed
list_recent(a, b) // List my content, from a’th to b’th most recent
list_public_feed(a, b) // List my followees’ content, from a’th to b’th most recent
Redis State
To pull this off, I mostly abuse a distributed hashmap that maps string keys to what Redis calls a SortedSet. A sorted set is a lot like a SQL index, allowing logarithmic lookups and adds. Notice though, that there’s a HUGE difference swimming through the log of all content size (typical in naive SQL) versus the log of a single user’s SortedSet size, specific to that user. You’ll see user Id’s and content Id’s as strings mapping to, essentially, their own personal mini SQL indices.
Let:
G[Cid]=C denote the Redis (G)lobal map between content Id and the content itself.
M[Uid]=Cid denote the Redis map between (M)y user Id and a SortedSet of their own personal content Ids, sorted by content Id
P[Uid]=Cid denote the Redis map between my user Id and a SortedSet of (P)ublic content Ids from users you’re following, sorted by content Id, similar to Facebook’s wall.
F[Uid]=Uid denote the Redis map between user Id and a SortedSet of user Ids that are (F)ollowing the user id key.
API Implementation
It’s assumed that every API below has your user Id to hash into Redis maps with.
create(c):
- Let cid = new unique, autoincrementing Id
- Put cid, c in G
- Put my uid, cid in M
- Get all my followers F[uid], and insert cid into all followers’ P[fid]
O( 1 + 1 + log( |M[uid]| ) ) = O( log( |M[uid]| ) ) online work (steps 1-3)
O( |F[uid]| * log( |P[fid]| ) ) offline work (step 4)
read(cid):
- Read from G[cid]
O(1)
update(cid, c):
- Put c in G[cid]
O(1)
delete(cid):
- Delete cid from G
- Delete cid from M
- Get all my followers F[uid], and delete cid from all followers’ P[fid]
O( 1 + log( |M[uid]| ) ) = O( log( |M[uid]| ) ) online work (steps 1-2)
O( |F[uid]| * log( |P[fid]| ) ) offline work (step 3)
follow(fid):
- Add uid to F[fid]
- For all cid’s in M[fid], add cid to P[uid]
O(1) Online work (step 1)
O( |M[fid]| * log( |P[uid]| ) ) Offline work (step 2)
unfollow(fid):
- Remove uid from F[fid]
- Let I = the intersection of P[uid] with M[fid] using ZINTERSTORE. These are the content Ids in my public content list that were created by user fid.
- For all cid’s in I, delete cid in P[uid]
O(1) Online work (step 1)
Assuming my public content list size P[uid] is greater than the followee’s content list size M[fid]:
O( |M[fid]| + |M[fid]|*log(|M[fid]|)) + |M[fid]| * log( |P[uid]| ) ) =
O( |M[fid]| * (log( |P[uid]| ) + log(|M[fid]|)) ) =
O( |M[fid]| * log( |P[uid]| ) ) Offline work (steps 2-3)
list_recent(a, b):
- Let k = b – a, the constant page size
- Lookup k elements from M[uid] using ZRANGE
O( log( |M[uid]| + k ) = O( log(|M[uid]|) )
list_public_feed(a, b)
- Let k = b – a, the constant page size
- Lookup k elements from P[uid] using ZRANGE
O( log( |P[uid]| + k ) = O( log(|P[uid]|) )
Summary
So if we cheat a bit and allow the system to be eventually-consistent (ex.: content won’t show up in follower lists immediately after publishing), we can push all 4 worst case costs of order N*log(N) into offline processes in the API call. Remember that the N in O( N * log(N) ) is only specific to 1 user’s scale, so this is much better than it sounds! This all lets us get away with only 4 simple structures that serve either constant or (user-scale) logarithmic online time for all operations.