@@ -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
5858Zoom 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
6161At this time, Zoom can be considered safe for use in low-traffic production
6262applications. However, as with any relatively new package, it is possible that
@@ -97,15 +97,15 @@ Zoom might be a good fit if:
9797
9898Zoom 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
111111Installation
@@ -197,7 +197,7 @@ To clarify, all you have to do to implement the `Model` interface is add a gette
197197for a unique id property.
198198
199199If 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
201201the first time the ` ModelId ` method is called iff it does not already have an id. The pseudo-randomly
202202generated id consists of the current UTC unix time with second precision, an incremented atomic
203203counter, 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
350350the 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
580581easy to fix the problem with the ` redis-check-aof ` tool, which will remove the partial transaction
581582from 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
584585you also make everything atomic. If you do not, Zoom can no longer guarantee that its indexes are
585586consistent. For example, if you change the value of a field which is indexed, you should also
586587update 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.
607616func 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
628637the 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.
637679func 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
661707Read more about:
662708- [ Redis Commands] ( http://redis.io/commands )
0 commit comments