<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Redis on Murat Useinov</title>
    <link>https://useinov.com/tags/redis/</link>
    <description>Recent content in Redis on Murat Useinov</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 05 May 2022 20:35:00 +0300</lastBuildDate>
    <atom:link href="https://useinov.com/tags/redis/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Redis 7 functions, same old rule</title>
      <link>https://useinov.com/posts/2022/redis-seven-functions/</link>
      <pubDate>Thu, 05 May 2022 20:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/redis-seven-functions/</guid>
      <description>&lt;p&gt;&lt;code&gt;FCALL update_if_newer 1 user:42 17&lt;/code&gt;. One round trip, a name instead of a SHA, and it survives a restart. Redis 7 came out last week, and Functions are the feature I care about. Everything EVAL scripts should have been.&lt;/p&gt;
&lt;p&gt;The problem they solve is real. Take compare-and-update: set a value only if the stored version is older. From PHP it is three round trips and a race between them. WATCH/MULTI works but is ugly and retries under contention. A Lua script does it atomically in one trip, and that is why we all wrote Lua scripts. But scripts are anonymous blobs. The application carries the source, sends it on every deploy, and debugging starts with &amp;ldquo;which SHA is this&amp;rdquo;. Functions fix the operational part: the library lives in Redis, has a name, has a version.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Redis Streams is a log, not a better queue</title>
      <link>https://useinov.com/posts/2021/redis-streams-log/</link>
      <pubDate>Fri, 20 Aug 2021 22:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/redis-streams-log/</guid>
      <description>&lt;p&gt;&amp;ldquo;Should we move the queue to Streams? They are the proper way now.&amp;rdquo; A colleague, this week, about our Redis list queue. Wrong question. Streams are not a newer version of the same thing.&lt;/p&gt;
&lt;p&gt;A list queue is a hand-off. LPUSH on one side, BRPOP on the other, and once a consumer takes the message, it is gone. Crash after the pop and the message died with you, unless you built the pending-list dance yourself. No history, no second reader. For &amp;ldquo;send this email eventually&amp;rdquo; it is honestly enough.&lt;/p&gt;</description>
    </item>
    <item>
      <title>TTL is not an invalidation strategy</title>
      <link>https://useinov.com/posts/2021/ttl-not-invalidation/</link>
      <pubDate>Wed, 24 Feb 2021 22:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/ttl-not-invalidation/</guid>
      <description>&lt;p&gt;Support ticket: a user changed his name, the site still shows the old one. The profile sits in Redis with TTL one hour. Fine, we set five minutes. A week later the hit rate drops and MySQL feels it. This seesaw has no good position. TTL is insurance, not invalidation.&lt;/p&gt;
&lt;p&gt;The obvious upgrade is cache-aside with explicit delete. Read: try the cache, miss, load from the database, write to the cache. Write: update the row, delete the key. Looks correct. It has a race.&lt;/p&gt;</description>
    </item>
    <item>
      <title>A Redis lock is a promise you cannot fully keep</title>
      <link>https://useinov.com/posts/2020/redis-lock-honesty/</link>
      <pubDate>Wed, 26 Feb 2020 21:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/redis-lock-honesty/</guid>
      <description>&lt;p&gt;Two workers, one order, processed twice. Every project gets this day. Someone writes &lt;code&gt;SETNX&lt;/code&gt;, calls it a distributed lock, closes the ticket. I want to slow down here, because the ticket is not closed.&lt;/p&gt;
&lt;p&gt;The small bugs first. A lock needs a TTL, or a crashed worker holds it forever. A lock needs an owner token, or worker A releases the lock of worker B:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$token&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;bin2hex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;random_bytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$ok&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$redis&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;lock:order:&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$orderId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$token&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;nx&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;ex&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the release must be atomic: compare the token and delete in one Lua script. Check in PHP, delete in a second command, and there is a gap. Something will land in that gap.&lt;/p&gt;</description>
    </item>
    <item>
      <title>What Redis does when memory ends</title>
      <link>https://useinov.com/posts/2019/redis-maxmemory/</link>
      <pubDate>Tue, 02 Apr 2019 15:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/redis-maxmemory/</guid>
      <description>&lt;p&gt;Users logged out at random. Not all, not always. Only sometimes, only in the afternoon. Afternoon is when traffic peaks. Traffic peaks fill the cache. The cache lived in the same Redis as the sessions, the instance hit &lt;code&gt;maxmemory&lt;/code&gt;, and the eviction policy was &lt;code&gt;allkeys-lru&lt;/code&gt;. Redis did exactly what we asked: threw away the least recently used keys, and some of them were sessions of people who went for lunch.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Horizon, or the queue becomes visible</title>
      <link>https://useinov.com/posts/2018/laravel-horizon-queues/</link>
      <pubDate>Sat, 20 Jan 2018 10:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/laravel-horizon-queues/</guid>
      <description>&lt;p&gt;&lt;code&gt;redis-cli llen queues:default&lt;/code&gt; and hope. That was my queue monitoring for years. Horizon replaces it with a real dashboard, and I did not know how much I needed one until I saw it.&lt;/p&gt;
&lt;p&gt;The mental shift matters more than the UI. &lt;code&gt;dispatch()&lt;/code&gt; is the beginning of the work, not the end. The job still has to wait in Redis, run, maybe fail, maybe retry. All of that was invisible. Now it is on one screen: throughput, wait time per queue, failed jobs with the full payload and the exception.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Cache stampede: the hot key problem</title>
      <link>https://useinov.com/posts/2017/cache-stampede/</link>
      <pubDate>Tue, 07 Feb 2017 21:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/cache-stampede/</guid>
      <description>&lt;p&gt;Cache hit rate 98 percent. The database still fell over. Both facts are true, and the second one does not care about the first.&lt;/p&gt;
&lt;p&gt;What happened. One key holds the result of a heavy query for the main page. TTL one minute. At some second the key expires. In that second a few hundred requests all get a miss, and all of them go to recompute the same heavy query. Redis is fine. MySQL is not. This is a cache stampede, and hit rate will not warn you, because hit rate is an average. The stampede lives in the worst second.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Redis cache and Redis queue are different databases</title>
      <link>https://useinov.com/posts/2015/redis-two-jobs/</link>
      <pubDate>Fri, 21 Aug 2015 19:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/redis-two-jobs/</guid>
      <description>&lt;p&gt;Two lines from redis.conf on one project:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;maxmemory 2gb
maxmemory-policy allkeys-lru
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Correct for a cache. Then sessions moved into the same instance, because Redis was already there. Then the job queue, same reason. One process, three tenants. This works until the day it does not.&lt;/p&gt;
&lt;p&gt;Cache data is disposable by definition, and those two lines embrace that. Memory fills up, Redis evicts the coldest keys, the application rebuilds them on demand. Persistence is optional. After a restart a cold cache is an inconvenience, not an incident.&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
