[{"data":1,"prerenderedAt":358},["ShallowReactive",2],{"blog-redis-caching-patterns":3},{"id":4,"title":5,"body":6,"date":345,"description":38,"excerpt":346,"extension":347,"meta":348,"navigation":60,"path":349,"seo":350,"stem":351,"tags":352,"__hash__":357},"blog\u002Fblog\u002Fredis-caching-patterns.md","Redis Caching Patterns for Laravel APIs: Beyond Cache::remember()",{"type":7,"value":8,"toc":336},"minimark",[9,19,25,29,32,80,83,87,90,179,183,186,235,238,242,253,282,285,289,292,299,323,329,332],[10,11,13,14,18],"h2",{"id":12},"why-cacheremember-isnt-enough","Why ",[15,16,17],"code",{},"Cache::remember()"," Isn't Enough",[20,21,22,24],"p",{},[15,23,17],{}," is great for simple cases. Production cache architecture requires thinking about key namespacing, TTL strategy, stampede protection, and invalidation. Getting any of these wrong under load can cause more damage than having no cache at all.",[10,26,28],{"id":27},"key-namespacing-strategy","Key Namespacing Strategy",[20,30,31],{},"Flat cache keys are a ticking time bomb. They collide across deployments, tenants, and feature versions.",[33,34,39],"pre",{"className":35,"code":36,"language":37,"meta":38,"style":38},"language-php shiki shiki-themes github-light github-dark","\u002F\u002F Bad: flat, unversioned, will collide\nCache::remember('users', 3600, fn() => User::all());\n\n\u002F\u002F Good: versioned, tenant-scoped, descriptive\n$key = \"v2:tenant:{$tenantId}:users:active:page:{$page}\";\nCache::remember($key, 3600, fn() => ...);\n","php","",[15,40,41,49,55,62,68,74],{"__ignoreMap":38},[42,43,46],"span",{"class":44,"line":45},"line",1,[42,47,48],{},"\u002F\u002F Bad: flat, unversioned, will collide\n",[42,50,52],{"class":44,"line":51},2,[42,53,54],{},"Cache::remember('users', 3600, fn() => User::all());\n",[42,56,58],{"class":44,"line":57},3,[42,59,61],{"emptyLinePlaceholder":60},true,"\n",[42,63,65],{"class":44,"line":64},4,[42,66,67],{},"\u002F\u002F Good: versioned, tenant-scoped, descriptive\n",[42,69,71],{"class":44,"line":70},5,[42,72,73],{},"$key = \"v2:tenant:{$tenantId}:users:active:page:{$page}\";\n",[42,75,77],{"class":44,"line":76},6,[42,78,79],{},"Cache::remember($key, 3600, fn() => ...);\n",[20,81,82],{},"Add a version prefix. When your data model changes, bump the version — old keys expire naturally without a manual flush.",[10,84,86],{"id":85},"cache-stampede-protection","Cache Stampede Protection",[20,88,89],{},"When a cached item expires and 1,000 concurrent requests try to regenerate it simultaneously, your database dies. Laravel's atomic locks solve this cleanly:",[33,91,93],{"className":35,"code":92,"language":37,"meta":38,"style":38},"$data = Cache::get($key);\nif (!$data) {\n    $lock = Cache::lock(\"lock:{$key}\", 10);\n    try {\n        if ($lock->get()) {\n            $data = expensiveQuery();\n            Cache::put($key, $data, 3600);\n        } else {\n            $lock->block(5); \u002F\u002F wait up to 5s for the other process\n            $data = Cache::get($key);\n        }\n    } finally {\n        optional($lock)->release();\n    }\n}\n",[15,94,95,100,105,110,115,120,125,131,137,143,149,155,161,167,173],{"__ignoreMap":38},[42,96,97],{"class":44,"line":45},[42,98,99],{},"$data = Cache::get($key);\n",[42,101,102],{"class":44,"line":51},[42,103,104],{},"if (!$data) {\n",[42,106,107],{"class":44,"line":57},[42,108,109],{},"    $lock = Cache::lock(\"lock:{$key}\", 10);\n",[42,111,112],{"class":44,"line":64},[42,113,114],{},"    try {\n",[42,116,117],{"class":44,"line":70},[42,118,119],{},"        if ($lock->get()) {\n",[42,121,122],{"class":44,"line":76},[42,123,124],{},"            $data = expensiveQuery();\n",[42,126,128],{"class":44,"line":127},7,[42,129,130],{},"            Cache::put($key, $data, 3600);\n",[42,132,134],{"class":44,"line":133},8,[42,135,136],{},"        } else {\n",[42,138,140],{"class":44,"line":139},9,[42,141,142],{},"            $lock->block(5); \u002F\u002F wait up to 5s for the other process\n",[42,144,146],{"class":44,"line":145},10,[42,147,148],{},"            $data = Cache::get($key);\n",[42,150,152],{"class":44,"line":151},11,[42,153,154],{},"        }\n",[42,156,158],{"class":44,"line":157},12,[42,159,160],{},"    } finally {\n",[42,162,164],{"class":44,"line":163},13,[42,165,166],{},"        optional($lock)->release();\n",[42,168,170],{"class":44,"line":169},14,[42,171,172],{},"    }\n",[42,174,176],{"class":44,"line":175},15,[42,177,178],{},"}\n",[10,180,182],{"id":181},"redis-hash-for-partial-updates","Redis Hash for Partial Updates",[20,184,185],{},"Instead of caching a full user object as a serialized blob (requiring a full rewrite on any change), use Redis hashes:",[33,187,189],{"className":35,"code":188,"language":37,"meta":38,"style":38},"Redis::hmset(\"user:{$id}\", [\n    'name'  => $user->name,\n    'email' => $user->email,\n    'plan'  => $user->plan,\n]);\nRedis::expire(\"user:{$id}\", 3600);\n\n\u002F\u002F Update just the plan without touching the rest of the cached data\nRedis::hset(\"user:{$id}\", 'plan', 'enterprise');\n",[15,190,191,196,201,206,211,216,221,225,230],{"__ignoreMap":38},[42,192,193],{"class":44,"line":45},[42,194,195],{},"Redis::hmset(\"user:{$id}\", [\n",[42,197,198],{"class":44,"line":51},[42,199,200],{},"    'name'  => $user->name,\n",[42,202,203],{"class":44,"line":57},[42,204,205],{},"    'email' => $user->email,\n",[42,207,208],{"class":44,"line":64},[42,209,210],{},"    'plan'  => $user->plan,\n",[42,212,213],{"class":44,"line":70},[42,214,215],{},"]);\n",[42,217,218],{"class":44,"line":76},[42,219,220],{},"Redis::expire(\"user:{$id}\", 3600);\n",[42,222,223],{"class":44,"line":127},[42,224,61],{"emptyLinePlaceholder":60},[42,226,227],{"class":44,"line":133},[42,228,229],{},"\u002F\u002F Update just the plan without touching the rest of the cached data\n",[42,231,232],{"class":44,"line":139},[42,233,234],{},"Redis::hset(\"user:{$id}\", 'plan', 'enterprise');\n",[20,236,237],{},"This pattern eliminates the cache invalidation problem for objects with multiple independent fields.",[10,239,241],{"id":240},"sorted-sets-for-leaderboards-and-feeds","Sorted Sets for Leaderboards and Feeds",[20,243,244,245,248,249,252],{},"Redis sorted sets (",[15,246,247],{},"ZADD",", ",[15,250,251],{},"ZRANGE",") are ideal for ranked data:",[33,254,256],{"className":35,"code":255,"language":37,"meta":38,"style":38},"\u002F\u002F Increment a user's score\nRedis::zadd('leaderboard:monthly', ['increment' => true], 10, \"user:{$userId}\");\n\n\u002F\u002F Fetch top 10\n$top10 = Redis::zrevrange('leaderboard:monthly', 0, 9, 'WITHSCORES');\n",[15,257,258,263,268,272,277],{"__ignoreMap":38},[42,259,260],{"class":44,"line":45},[42,261,262],{},"\u002F\u002F Increment a user's score\n",[42,264,265],{"class":44,"line":51},[42,266,267],{},"Redis::zadd('leaderboard:monthly', ['increment' => true], 10, \"user:{$userId}\");\n",[42,269,270],{"class":44,"line":57},[42,271,61],{"emptyLinePlaceholder":60},[42,273,274],{"class":44,"line":64},[42,275,276],{},"\u002F\u002F Fetch top 10\n",[42,278,279],{"class":44,"line":70},[42,280,281],{},"$top10 = Redis::zrevrange('leaderboard:monthly', 0, 9, 'WITHSCORES');\n",[20,283,284],{},"No SQL aggregate query. No locks. O(log N) writes, O(log N + M) reads.",[10,286,288],{"id":287},"cache-invalidation-strategies","Cache Invalidation Strategies",[20,290,291],{},"The two practical approaches:",[20,293,294,298],{},[295,296,297],"strong",{},"Tag-based invalidation"," (Laravel's built-in support):",[33,300,302],{"className":35,"code":301,"language":37,"meta":38,"style":38},"Cache::tags(['posts', \"user:{$userId}\"])->remember($key, 3600, fn() => ...);\n\n\u002F\u002F Later, flush everything tagged 'posts'\nCache::tags(['posts'])->flush();\n",[15,303,304,309,313,318],{"__ignoreMap":38},[42,305,306],{"class":44,"line":45},[42,307,308],{},"Cache::tags(['posts', \"user:{$userId}\"])->remember($key, 3600, fn() => ...);\n",[42,310,311],{"class":44,"line":51},[42,312,61],{"emptyLinePlaceholder":60},[42,314,315],{"class":44,"line":57},[42,316,317],{},"\u002F\u002F Later, flush everything tagged 'posts'\n",[42,319,320],{"class":44,"line":64},[42,321,322],{},"Cache::tags(['posts'])->flush();\n",[20,324,325,328],{},[295,326,327],{},"Event-driven invalidation",": Listen for model events and explicitly delete affected keys. More granular, more code, but no risk of tag-based flush wiping unrelated data.",[20,330,331],{},"Choose tags for simplicity, events for precision.",[333,334,335],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":38,"searchDepth":51,"depth":51,"links":337},[338,340,341,342,343,344],{"id":12,"depth":51,"text":339},"Why Cache::remember() Isn't Enough",{"id":27,"depth":51,"text":28},{"id":85,"depth":51,"text":86},{"id":181,"depth":51,"text":182},{"id":240,"depth":51,"text":241},{"id":287,"depth":51,"text":288},"2024-10-02",null,"md",{},"\u002Fblog\u002Fredis-caching-patterns",{"title":5,"description":38},"blog\u002Fredis-caching-patterns",[353,354,355,356],"Redis","Laravel","PHP","Caching","zu8lce5xoIIz1S5zueMC38N-5RRikwPb5z2Q8Vomz9Y",1779430667060]