Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Document Locking with Redis in symfony2

Tom
November 19, 2013

Document Locking with Redis in symfony2

A demonstration of an approach we used to disallow concurrent editing of records in our application. We used Redis, symfony2 and Doctrine2

Tom

November 19, 2013
Tweet

More Decks by Tom

Other Decks in Programming

Transcript

  1. • Cloud hosted SaaS product • Provide staff rostering primarily

    for hospitality companies • Focus on rostering staff to ad hoc events, not regular schedules • 3 founders - I’m the IT guy
  2. The problem • Multiple users editing an event at the

    same time • Resulting in: • Lost work • Unhappy customers
  3. Solutions • Do nothing, let our customers figure it out

    • we might not have any customers • Store locks in our mysql database • edge cases are ugly
  4. Redis - A better way Redis is an open source,

    BSD licensed, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.
  5. Redis… • Developed by Salvatore Sanfilippo (@antirez) • An in-memory

    data store with optional disk persistence • Clusters easily • Documentation is amazing! • Has myriad uses beyond what I am discussing today • Take a look, it will open your eyes to new solutions to age old problems
  6. Using Redis • predis is the de facto standard library

    for PHP • can be further accelerated with a C extension • As a key value store Redis has a very simple command set • We will be focusing on a handful of commands today
  7. How did we do it? • Create a set using

    the event id to form a key name • Populate the set with the ids of users who have opened a read lock
  8. Edit action <?php class EventController extends Controller { ! public

    function editAction($event){ //... ! //Check for an existing lock and redirect if one exists $redisKey = 'event_edit_'.$event->getId(); $redis = $this->container->get('snc_redis.default'); if ($redis->exists($redisKey)) { if (!$redis->sismember($redisKey, $this->getUser()->getId())){ return $this->redirect($this->generateUrl(‘event_edit_conflict’, array('id' => $id))); } } //Create a lock for this user $redis->sadd($redisKey, $this->getUser()->getId()); $redis->expire($redisKey, 120); } //...
  9. Releasing a lock public function updateAction(Request $request, $id) { //$businessLogic->doStuff($event)

    //etc... ! //Once the changes have been flushed to the database release the lock $redis = $this->container->get('snc_redis.default'); $redis->srem('event_edit_'.$event->getId(), $this->getUser()->getId()); ! //... }
  10. Maintaining a lock while editing Client Side ! function pollEditLock()

    { $.get( '{{ path(‘event_edit_lock’, { 'id': entity.id })}}’, function(data) { //Do something } ); } ! setInterval(pollEditLock, 30 * 1000); ! Server Side ! <?php //... public function editLockAction(Event $event) { //... $redisKey = 'event_edit_'.$event->getId(); $redis = $this->container->get('snc_redis.default'); ! $redis->sadd($redisKey, $this->getUser()->getId()); $redis->expire($redisKey, self::LOCK_TIME); ! return new JsonResponse(array('ok')); } //...
  11. Sometimes users want to edit concurrently public function editConflictAction(Request $request,

    $id) { //... ! $redisKey = 'event_edit_'.$entity->getId(); $redis = $this->container->get('snc_redis.default'); ! if ($request->query->get('force')) { $redis->sadd($redisKey, $this->getUser()->getId()); $redis->expire($redisKey, self::LOCK_TIME); ! return $this->redirect($this->generateUrl('event_edit', array('id' => $id))); } $users = array(); $client = $this->get('context.client'); foreach ($redis->smembers($redisKey) as $userId){ $users[] = $em->getRepository(‚ÀòRCRosterBundle:User‚ÀÙ)->find($userId); } ! //... }