We're in the process of moving a fairly large, high-traffic website from AppFabric onto Redis. We're using .NET and the StackExchange.Redis library. One area I'm a bit stumped on is distributed locks, which we rely on quite a bit. In AppFabric, the typical pattern is to:
- Call
GetAndLockwhich will lock a key and return a lock handle. - Update some stuff in the key using C# code
- Call
PutAndUnlock, passing in that same lock handle
If another thread/process/host tries to update the same data, its GetAndLock call will fail and it has some logic to wait a few milliseconds and retry x amount of times.
I'm trying to determine the best way to do this using Redis, which doesn't appear to support distributed locks natively.
For the most part, what we're in need of is a way to update a single property of a serialized POCO object in the cache. For example, if we had a data structure with an enumeration of users serialized in our cache, and we wanted to update a single property of one of those users, we'd lock the key, update the property on that one user, serialize it back, and unlock the key. This would prevent an update to another user being lost if another machine was updating another user.
So, I did a bit of research on this topic and there's a few solutions that seem promising, or at least learning more about.
StackExchange.Redis Locks
This path seemed to be a dead end. The LockTake and LockRelease don't appear to lock a specific resource, they just appear to be building blocks for implementing a locking system of some sort. I couldn't find enough documentation on this is really figure out if it could be useful in my scenario.
RedLock.net
I found the RedLock.net library, which is built on StackExchange.Redis. It appears to implement a distributed locking system somewhat similar to how AppFabric works. You can acquire a lock on a resource, get some sort of handle, update stuff, then unlock that resource.
However, using this would mean bringing in a new library and also seems rather complicated to set up (totally different way of setting up endpoints and configuring things). Reading about it also sounds like it could be slow; the lock is only acquired if the lock can be distributed to at least half the machines. I also found a lot of people saying unless you're doing something really complicated, you shouldn't need this.
Transactions
The StackExchange.Redis documentation on Transactions seemed rather interesting. It's well known you can execute Redis commands as atomic units using transactions, and they'll be queued up and executed all at once. However, update a serialized JSON property of an object is not a Redis command, and I can't call back to C# code when that transaction gets executed. So, this doesn't seem to work. But wait, there is this concept they go over involving watching a key. If it changes, we abort our transaction and roll back. I'm wondering if I could take advantage of that mechanism and just roll back if the key changes, and retry. This seems like it might be a little too optimistic on keys that a lot of things are fighting over and would result in a lot of retries instead of "waiting" to acquire a lock.
Manually tracking versions of cache values
Another thing AppFabric had that was nice is CacheVersion properties of every cached value. You could look at the value of something in the cache and see its version. If the version had changed since you read it, you could know it was dirty and reload it. We use this quite a bit, and I was thinking about implementing my own serialization in Redis which would serialize a unique version property along with everything in the cache. If I did this, I could perhaps add some sort of constraint to the transaction to abort if the version didn't match what I originally loaded. This might run into a lot of the same issues as the previous idea.
Has anyone else found a good solution to this sort of problem? Obviously the best solution would be to break apart these larger objects into a bunch of tiny, individual keys we can update atomically, but that's unfortunately completely unfeasible at the moment.