Slide 1

Slide 1 text

Scaling PHP Applications With Redis Josh Butts Nomad PHP, July 2015 https://joind.in/14896

Slide 2

Slide 2 text

About Me • VP of Engineering at Offers.com in Austin, Texas • Long-time Austin PHP organizer • Competitive Skee-baller • Follow me online: • github.com/jimbojsb • @jimbojsb

Slide 3

Slide 3 text

Agenda • Redis Overview • Types of keys • PHP Strategies

Slide 4

Slide 4 text

What is Redis? • Redis is an “Advanced” key-value store database • Backed by VMWare • Redis works like memcached, but better: • Atomic commands & transactions • Server-side data structures • Comparable speed

Slide 5

Slide 5 text

Redis in an in-memory database • You need as much RAM as you have data • Disk persistence is exactly that • Customize disk writes interval to suit your pain threshold • Something interesting happens when you run out of memory

Slide 6

Slide 6 text

Redis is Single-Threaded • Redis uses an event-loop style architecture, like node, nginx • It can peg one CPU core but that’s it • Redis Cluster can solve this with automatic sharding • Below Redis 3.0, it’s easier to get a bigger server than fix your code (In my opinion)

Slide 7

Slide 7 text

How to get Redis • http://redis.io • Doesn’t run (well) on Windows • Has virtually no compile dependencies • apt-get, homebrew, yum, etc • Amazon ElastiCache • No reason not to run the latest and greatest • but at get at least 2.6

Slide 8

Slide 8 text

How to explore • There aren’t any good GUI tools out there • redis-cli is your friend

Slide 9

Slide 9 text

A bit about organization • Redis can have multiple databases • use one database per application • Within a database, namespace your keys • Ex: classname:uid:datafield • Keep them concise but useful. Keys take memory too!

Slide 10

Slide 10 text

Types of Keys

Slide 11

Slide 11 text

String Keys • Simple key-value • Memcache equivalent • Bitwise ops in newer versions • Common Commands • SET • GET • INCR http://redis.io/commands#string

Slide 12

Slide 12 text

Hash Keys • Key + multiple fields / values • Think 1-dimensional associative array • mykey => [field1 => value1, field2 => value2] http://redis.io/commands#hash • Common commands • HSET • HGET • HGETALL • HDEL • HVALS • HKEYS

Slide 13

Slide 13 text

Hash Keys

Slide 14

Slide 14 text

Set Keys • key + unordered list of strings • myset => [item2, item5, item1] • Common Commands • SADD • SMEMBERS • SISMEMBER • SREM http://redis.io/commands#set

Slide 15

Slide 15 text

Set Keys

Slide 16

Slide 16 text

List Keys • Like sets, except insertion order matters • Build queues or stacks • Optional blocking • Common commands • RPUSH, LPUSH • RPOP, LPOP, BRPOP, BLPOP • LLEN http://redis.io/commands#list

Slide 17

Slide 17 text

List Keys

Slide 18

Slide 18 text

Sorted Set Keys • Like sets, but sorted by a user-provided score value • Extremely fast access by score or range of scores, because it’s sorted in storage • Common commands • ZADD • ZRANGE • ZREVRANGE

Slide 19

Slide 19 text

Sorted Set Keys

Slide 20

Slide 20 text

Other commands that work on all keys • DEL - delete a key, regardless of type • KEYS - search for keys (usually with a wildcard) • EXPIRE / PERSIST - change expiration values for keys

Slide 21

Slide 21 text

Connecting from PHP

Slide 22

Slide 22 text

Connecting from PHP • Several libraries out there • Just use Predis (https://github.com/nrk/predis) • Requires PHP 5.3

Slide 23

Slide 23 text

Predis • All the examples here assume you’re using Predis • Connect to a localhost redis: $p = new Predis\Client(); • Redis commands implemented as magic methods on Predis\Client • $p->set($key, $val); • $val = $p->get($key);

Slide 24

Slide 24 text

Examples

Slide 25

Slide 25 text

Attribute Display Logic items id INT(11) name VARCHAR(32) items_attributes id INT(11) item_id INT(11) attr_name VARCHAR(32) attr_value VARCHAR(32) 10k rows 100k rows • An offer, from Dell, has: • Free Shipping, Financing Available, Expires Soon, etc

Slide 26

Slide 26 text

Attribute Display Logic • Display “Free Shipping” graphic on the item if it has a free shipping attribute row

Slide 27

Slide 27 text

Attribute Display - Traditional class Item { public function hasAttribute($name) { $sql = "SELECT 1 FROM items_attributes WHERE item_id = $this->id AND attr_name='$name' LIMIT 1"; $result = $this->pdo->execute($sql); return $result != false; } }

Slide 28

Slide 28 text

Denormalize data from MySQL to Redis • Smart, selective caching • Define a consistent way to name a relational object in Redis • I prefer [object class]:[object id]:[attribute] • ex: product:13445:num_comments • This prevents data collisions, and makes it easy to work with data on the command line

Slide 29

Slide 29 text

Attribute Display - Redis class Item { public function hasAttribute($name) { return $this->redis->sismember(“item:$this->id:attributes”, $name); } public function addAttribute($name, $value) { //traditional mysql stuff here still $this->redis->sadd('item:$this->id:attributes', $name); } public function deleteAttribute($name) { //traditional mysql stuff here $this->redis->srem(‘item:$this->id:attributes’, $name); } }

Slide 30

Slide 30 text

Advantages • The more items you have, the less MySQL will scale this solution for you on it’s own • Frequently updating your items kills the MySQL query cache • Checking existence of a set member is O(1) time • On a mid-range laptop, I can check roughly 10,000 attributes per second

Slide 31

Slide 31 text

MVC Routing • Example: • Offers.com has over 10000 stores • Store pages live at /[store]/

Slide 32

Slide 32 text

Old Store Routing class Store_Route implements Zend_Controller_Router_Route_Interface { public function route($url) { $sql = “SELECT 1 FROM stores WHERE url=’$url’”; $store = $this->pdo->execute($sql); // make sure $store is properly validated and then... return array(“module” => “core”, “controller” => “store”, “action” => “index”); } }

Slide 33

Slide 33 text

New Routing class Redis_Route implements Zend_Controller_Router_Route_Interface { public function route($url) { $p = $this->predis; if ($p->exists($url)) { list($module, $controller, $action) = $this->redis->hvals($url); return array(“module” => $module, “controller” => $controller, “action” => $action); } return false; } }

Slide 34

Slide 34 text

Filling in the Redis keys class Store { public function create(array $data) { // ... traditional SQL stuff to put store in the database ... // $route = array(“module” => “core”, “controller” => “store”, “action” => “index”); $this->predis->hmset($data[“url”], $route); } }

Slide 35

Slide 35 text

Advantages • I can now create offers.com/[anything]/ and route it to the right place, in O(1) time • I’m only adding a few lines of code to my existing models • One custom Redis route can handle any dynamic url

Slide 36

Slide 36 text

Event Throttling • A common task is to only let a user do something N number of times • This becomes more complex when you add a time window to that constraint • We can use key expirations in Redis to help with this

Slide 37

Slide 37 text

A Simple Event Throttling Class class Event { public static function log($event, $uid, $duration = null) { $r = self::$redis; $r->incr("event:$event:$uid"); if ($duration) { if ($r->ttl("event:$event:$uid") < 0) { $r->expires("event:$event:$uid", $duration); } } } public static function isAllowed($event, $uid, $threshold = null) { $r = self::$redis; if ($r->exists("event:$event:$uid")) { if ($threshold) { $currentValue = $r->get("even:$event:$uid"); if ($currentValue <= $threshold) { return true; } } return false; } return true; } }

Slide 38

Slide 38 text

Using the Event Class class MyController { public function clickAction() { $userId = $_SESSION["uid"]; if (Event::isAllowed("click", $userId, 5)) { // magic happens here Event::log('click', $userId, 3600); } else { // send an error message } } }

Slide 39

Slide 39 text

Most Popular [insert widget here] • Everyone likes to vote on things • Facebook like? • You probably want to store this 2 ways • in MySQL for reporting, long term aggregation, etc • something faster so your website will actually work

Slide 40

Slide 40 text

Using Zsets for “Most Popular” zrevrange("widgets:popular", $rangeStart, $rangeEnd, true); } public function logVote() { $pdo->query(" UDPATE widgets SET votes=votes+1 WHERE id='$this->id' "); $redis->zincrby("widgets:popular", 1, $this->name); } }

Slide 41

Slide 41 text

Questions?

Slide 42

Slide 42 text

Job Queues • Redis lists make great job queues • Offload your intensive workloads to some other process • Blocking I/O allows you to easily build long-running daemons • Be aware of scale tradeoffs vs. data availability

Slide 43

Slide 43 text

Job Queues class Queue { protected $name; protected $predis; public function push($job) { $this->predis->lpush($this->name, json_encode($job)); } public function pop($block = false) { $job = null; if ($block) { $data = $this->predis->brpop($this->name, 0); $job = $data[1]; } else { $job = $this->predis->rpop($this->name); } if ($job) { return json_decode($job); } } }

Slide 44

Slide 44 text

Queuing Jobs $q = new Queue(‘test_queue'); if ($form->isValid($_POST)) { $q->push($form->getValues()); $message = “Thanks for your submission”; } else { $message = “Error - something went wrong”; } echo “

$message

”;

Slide 45

Slide 45 text

Processing Jobs - Crontab style function processJob(Array $job) { //...something really cool here // throw an exception on error } // process all pending jobs $q = new Queue(‘email_confirmations’); while ($job = $q->pop()) { try { processJob($job); } catch (Exception $e) { echo “error processing job”; $q = new Queue(‘errors’); $q->push($job); } }

Slide 46

Slide 46 text

Processing Jobs - Worker style function processJob(Array $job) { //...something really cool here // throw an exception on error } // keep processing jobs as they become available $q = new Queue(‘test_queue’); while ($job = $q->pop(true)) { try { processJob($job); } catch (Exception $e) { echo “error processing job”; $q = new Queue(‘errors’); $q->push($job); } }