Skip to content

Commit 6bb65c4

Browse files
committed
Update README for optimistic locking
1 parent a837248 commit 6bb65c4

File tree

1 file changed

+74
-28
lines changed

1 file changed

+74
-28
lines changed

README.md

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ Table of Contents
4242
- [More Information](#more-information)
4343
* [Persistence](#persistence)
4444
* [Atomicity](#atomicity)
45-
* [Concurrent Updates](#concurrent-updates)
45+
* [Concurrent Updates and Optimistic Locking](#concurrent-updates-and-optimistic-locking)
4646
- [Testing & Benchmarking](#testing---benchmarking)
47-
* [Running the Tests:](#running-the-tests-)
48-
* [Running the Benchmarks:](#running-the-benchmarks-)
47+
* [Running the Tests](#running-the-tests)
48+
* [Running the Benchmarks](#running-the-benchmarks)
4949
- [Contributing](#contributing)
5050
- [Example Usage](#example-usage)
5151
- [License](#license)
@@ -56,7 +56,7 @@ Development Status
5656
------------------
5757

5858
Zoom was first started in 2013. It is well-tested and going forward the API
59-
will be relatively stable. We are closing in on Version 1.0.0-alpha.
59+
will be relatively stable. We are closing in on Version 1.0.
6060

6161
At this time, Zoom can be considered safe for use in low-traffic production
6262
applications. However, as with any relatively new package, it is possible that
@@ -97,15 +97,15 @@ Zoom might be a good fit if:
9797

9898
Zoom might ***not*** be a good fit if:
9999

100-
1. **You are working with a lot of data.** Zoom stores all data in memory at all times, and does not
100+
1. **You are working with a lot of data.** Redis is an in-memory database, and Zoom does not
101101
yet support sharding or Redis Cluster. Memory could be a hard constraint for larger applications.
102102
Keep in mind that it is possible (if expensive) to run Redis on machines with up to 256GB of memory
103103
on cloud providers such as Amazon EC2.
104-
2. **You require the ability to run advanced queries.** Zoom currently only provides support for
105-
basic queries and is not as powerful or flexible as something like SQL. For example, Zoom currently
106-
lacks the equivalent of the `IN` or `OR` SQL keywords. See the
107-
[documentation](http://godoc.org/github.com/albrow/zoom/#Query) for a full list of the types of queries
108-
supported.
104+
2. **You need advanced queries.** Zoom currently only provides support for basic queries and is
105+
not as powerful or flexible as something like SQL. For example, Zoom currently lacks the
106+
equivalent of the `IN` or `OR` SQL keywords. See the
107+
[documentation](http://godoc.org/github.com/albrow/zoom/#Query) for a full list of the types
108+
of queries supported.
109109

110110

111111
Installation
@@ -197,7 +197,7 @@ To clarify, all you have to do to implement the `Model` interface is add a gette
197197
for a unique id property.
198198

199199
If you want, you can embed `zoom.RandomId` to give your model all the
200-
required methods. A struct with `zoom.RandomId` embedded will genrate a pseudo-random id for itself
200+
required methods. A struct with `zoom.RandomId` embedded will generate a pseudo-random id for itself
201201
the first time the `ModelId` method is called iff it does not already have an id. The pseudo-randomly
202202
generated id consists of the current UTC unix time with second precision, an incremented atomic
203203
counter, a unique machine identifier, and an additional random string of characters. With ids generated
@@ -349,7 +349,8 @@ if err := People.UpdateFields([]string{"Name"}, person); err != nil {
349349
`UpdateFields` uses "last write wins" semantics, so if another caller updates
350350
the same field, your changes may be overwritten. That means it is not safe for
351351
"read before write" updates. See the section on
352-
[Concurrent Updates](#concurrent-updates) for more information.
352+
[Concurrent Updates](#concurrent-updates-and-optimistic-locking) for more
353+
information.
353354

354355
### Finding a Single Model
355356

@@ -580,7 +581,7 @@ corrupted. If this happens, Redis will refuse to start until the AOF file is fix
580581
easy to fix the problem with the `redis-check-aof` tool, which will remove the partial transaction
581582
from the AOF file.
582583

583-
If you intend to issue custom Redis commands or run custom scripts, it is highly recommended that
584+
If you intend to issue Redis commands directly or run custom scripts, it is highly recommended that
584585
you also make everything atomic. If you do not, Zoom can no longer guarantee that its indexes are
585586
consistent. For example, if you change the value of a field which is indexed, you should also
586587
update the index for that field in the same transaction. The keys that Zoom uses for indexes
@@ -593,21 +594,29 @@ Read more about:
593594
- [Redis scripts](http://redis.io/commands/eval)
594595
- [Redis transactions](http://redis.io/topics/transactions)
595596

596-
### Concurrent Updates
597+
### Concurrent Updates and Optimistic Locking
597598

598-
Currently, Zoom does not directly support concurrent "read before write" updates
599-
on models. The `UpdateFields` method introduced in version 0.12 offers some
600-
additional safety for concurrent updates, as long as no concurrent callers
601-
update the same fields (or if you are okay with updates overwriting previous
602-
changes). However, cases where you need to do a "read before write" update are
603-
still not safe if you use a naive implementation. For example, consider the
604-
following code:
599+
Zoom 0.18.0 introduced support for basic optimistic locking. You can use
600+
optimistic locking to safely implement concurrent "read before write" updates.
601+
602+
Optimistic locking utilizes the `WATCH`, `MULTI`, and `EXEC` commands in Redis
603+
and only works in the context of transactions. You can use the
604+
[`Transaction.Watch`](https://godoc.org/github.com/albrow/zoom#Transaction.Watch)
605+
method to watch a model for changes. If the model changes after you call `Watch`
606+
but before you call `Exec`, the transaction will not be executed and instead
607+
will return a
608+
[`WatchError`](https://godoc.org/github.com/albrow/zoom#WatchError). You can
609+
also use the `WatchKey` method, which functions exactly the same but operates on
610+
keys instead of models.
611+
612+
To understand why optimistic locking is useful, consider the following code:
605613

606614
``` go
615+
// likePost increments the number of likes for a post with the given id.
607616
func likePost(postId string) error {
608617
// Find the Post with the given postId
609618
post := &Post{}
610-
if err := Posts.Find(postId); err != nil {
619+
if err := Posts.Find(postId, post); err != nil {
611620
return err
612621
}
613622
// Increment the number of likes
@@ -628,15 +637,48 @@ multiple machines concurrently, because the `Post` model can change in between
628637
the time we retrieved it from the database with `Find` and saved it again with
629638
`Save`.
630639

631-
However, since Zoom allows you to run your own Redis commands, you could fix
632-
this code by manually using HINCRBY:
640+
You can use optimistic locking to avoid this problem. Here's the revised code:
641+
642+
```go
643+
// likePost increments the number of likes for a post with the given id.
644+
func likePost(postId string) error {
645+
// Start a new transaction and watch the post key for changes. It's important
646+
// to call Watch or WatchKey *before* finding the model.
647+
tx := pool.NewTransaction()
648+
if err := tx.WatchKey(Posts.ModelKey(postId)); err != nil {
649+
return err
650+
}
651+
// Find the Post with the given postId
652+
post := &Post{}
653+
if err := Posts.Find(postId, post); err != nil {
654+
return err
655+
}
656+
// Increment the number of likes
657+
post.Likes += 1
658+
// Save the post in a transaction
659+
tx.Save(Posts, post)
660+
if err := tx.Exec(); err != nil {
661+
// If the post was modified by another goroutine or server, Exec will return
662+
// a WatchError. You could call likePost again to retry the operation.
663+
return err
664+
}
665+
}
666+
```
667+
668+
Optimistic locking is not appropriate for models which are frequently updated,
669+
because you would almost always get a `WatchError`. In fact, it's called
670+
"optimistic" locking because you are optimistically assuming that conflicts will
671+
be rare. That's not always a safe assumption.
672+
673+
Don't forget that Zoom allows you to run Redis commands directly. This
674+
particular problem might be best solved by the `HINCRBY` command.
633675

634676
```go
635677
// likePost atomically increments the number of likes for a post with the given
636678
// id and then returns the new number of likes.
637679
func likePost(postId string) (int, error) {
638680
// Get the key which is used to store the post in Redis
639-
postKey := Posts.ModelKey(postId)
681+
postKey := Posts.ModelKey(postId, post)
640682
// Start a new transaction
641683
tx := pool.NewTransaction()
642684
// Add a command to increment the number of Likes. The HINCRBY command returns
@@ -654,9 +696,13 @@ func likePost(postId string) (int, error) {
654696
}
655697
```
656698

657-
Future versions of Zoom may provide
658-
[optimistic locking](https://github.com/albrow/zoom/issues/13) or other means to
659-
make "read before write" updates easier.
699+
Finally, if optimistic locking is not appropriate and there is no built-in Redis
700+
command that offers the functionality you need, Zoom also supports custom Lua
701+
scripts via the
702+
[`Transaction.Script`](https://godoc.org/github.com/albrow/zoom#Transaction.Script)
703+
method. Redis is single-threaded and scripts are always executed atomically, so
704+
you can perform complicated updates without worrying about other clients
705+
changing the database.
660706

661707
Read more about:
662708
- [Redis Commands](http://redis.io/commands)

0 commit comments

Comments
 (0)