4.4.3 Purchasing items

  • Redis in Action – Home
  • Foreword
  • Preface
  • Acknowledgments
  • About this Book
  • About the Cover Illustration
  • Part 1: Getting Started
  • Part 2: Core concepts
  • Part 3: Next steps
  • Appendix A
  • Appendix B
  • Buy the paperback
  • Redis in Action – Home
  • Foreword
  • Preface
  • Acknowledgments
  • About this Book
  • About the Cover Illustration
  • Part 1: Getting Started
  • Part 2: Core concepts
  • Part 3: Next steps
  • Appendix A
  • Appendix B
  • Buy the paperback

    4.4.3 Purchasing items

    To process the purchase of an item, we first WATCH the market and the user who’s buying the item. We then fetch the buyer’s total funds and the price of the item, and verify that the buyer has enough money. If they don’t have enough money, we cancel the transaction. If they do have enough money, we perform the transfer of money between the accounts, move the item into the buyer’s inventory, and remove the item from the market. On WATCH error, we retry for up to 10 seconds in total. We can see the function which handles the purchase of an item in the following listing.

    Listing 4.6The purchase_item() function
    def purchase_item(conn, buyerid, itemid, sellerid, lprice):
       buyer = "users:%s"%buyerid
       seller = "users:%s"%sellerid
       item = "%s.%s"%(itemid, sellerid)
       inventory = "inventory:%s"%buyerid
       end = time.time() + 10
       pipe = conn.pipeline()
    
    

     

     

       while time.time() < end:
          try:
    

     

     

             pipe.watch("market:", buyer)
    
    

    Watch for changes to the market and to the buyer’s account information.

     

     

             price = pipe.zscore("market:", item)
             funds = int(pipe.hget(buyer, "funds"))
             if price != lprice or price > funds:
                pipe.unwatch()
    

    Check for a sold/repriced item or insufficient funds.

     

     

                return None
    
    

     

     

             pipe.multi()
             pipe.hincrby(seller, "funds", int(price))
             pipe.hincrby(buyer, "funds", int(-price))
             pipe.sadd(inventory, itemid)
             pipe.zrem("market:", item)
             pipe.execute()
    

    Transfer funds from the buyer to the seller, and transfer the item to the buyer.

     

     

             return True
    

     

     

          except redis.exceptions.WatchError:
             pass
    
    

    Retry if the buyer’s account or the market changed.

     

     

       return False
    

     

     

     

    To purchase an item, we need to spend more time preparing the data, and we need to watch both the market and the buyer’s information. We watch the market to ensure that the item can still be bought (or that we can notice that it has already been bought), and we watch the buyer’s information to verify that they have enough money. When we’ve verified that the item is still there, and that the buyer has enough money, we go about actually moving the item into their inventory, as well as moving money from the buyer to the seller.

    After seeing the available items in the market, Bill (user 27) decides that he wants to buy ItemM from Frank through the marketplace. Let’s follow along to see how our data changes through figures 4.5 and 4.6.

    If either the market ZSET or Bill’s account information changes between our WATCH and our EXEC, the purchase_item() function will either retry or abort, based on how long it has been trying to purchase the item, as shown in listing 4.6.

    WHY DOESN’T REDIS IMPLEMENT TYPICAL LOCKING?When accessing data for writing (SELECT FOR UPDATE in SQL), relational databases will place a lock on rows that are accessed until a transaction is completed with COMMIT or ROLLBACK. If any other client attempts to access data for writing on any of the same rows, that client will be blocked until the first transaction is completed. This form of locking works well in practice (essentially all relational databases implement it), though it can result in long wait times for clients waiting to acquire locks on a number of rows if the lock holder is slow.

    Because there’s potential for long wait times, and because the design of Redis minimizes wait time for clients (except in the case of blocking LIST pops), Redis doesn’t lock data during WATCH. Instead, Redis will notify clients if someone else modified the data first, which is called optimistic locking (the actual locking that relational databases perform could be viewed as pessimistic). Optimistic locking also works well in practice because clients are never waiting on the first holder of the lock; instead they retry if some other client was faster.

    Figure 4.5Before the item can be purchased, we must watch the market and the buyer’s information to verify that the item is still available, and that the buyer has enough money.
    Figure 4.6In order to complete the item purchase, we must actually transfer money from the buyer to the seller, and we must remove the item from the market while adding it to the buyer’s inventory.

    In this section, we’ve discussed combining WATCH, MULTI, and EXEC to handle the manipulation of multiple types of data so that we can implement a marketplace. Given this functionality as a basis, it wouldn’t be out of the question to make our marketplace into an auction, add alternate sorting options, time out old items in the market, or even add higher-level searching and filtering based on techniques discussed in chapter 7.

    As long as we consistently use transactions in Redis, we can keep our data from being corrupted while being operated on by multiple clients. Let’s look at how we can make our operations even faster when we don’t need to worry about other clients altering our data.