Asmir Mustafic
GetYourGuide
There are only two hard things in Computer Science: cache invalidation and naming things.
Cache
Contento
Headless CMS
webhook
get
get / set
get
legal, email templates, help center and much more
Why is hard?
| key | value |
|---|---|
| 1 | {id:1, slug: "foo", loc: 1} |
| 2 | {id:2, slug: "bar", loc: 1} |
@Get("/user/{id}")
fun getUserById(id) {
user = cache.get(id)
if (user == null) {
user = repository.getUserById(id);
cache.put(id, user)
}
return user;
}
| key | value |
|---|---|
| 1 | {id:1, slug: "foo", loc: 1} |
| 2 | {id:2, slug: "bar", loc: 1} |
fun onUserUpdate(user) {
cache.delete(user.id)
}| key | value |
|---|---|
| 1 | {id:1, mail: "foo@a", loc: 1} |
| 2 | {id:2, mail: "bar@a", loc: 1} |
| foo@a | {id:1, mail: "foo@a", loc: 1} |
@Get("/user/{id}")
fun getUserById(id) {
user = cache.get(id)
if (user == null) {
user = repository.getUserById(id);
cache.put(id, user)
}
return user;
}
@Get("/user-by-mail/{mail}")
fun getUserByMail(mail) {
user = cache.get(mail)
if (user == null) {
user = repository.getUserByMail(mail);
cache.put(mail, user)
}
return user;
}fun onUserUpdate(user) {
cache.delete(user.id)
cache.delete(user.mail)
}| key | value |
|---|---|
| 1 | {id:1, mail: "foo@a", loc: 1} |
| 2 | {id:2, mail: "bar@a", loc: 1} |
| foo@a | {id:1, mail: "foo@a", loc: 1} |
| key | value |
|---|---|
| 1 | {id:1, mail: "foo@a", loc: 1} |
| 2 | {id:2, mail: "bar@a", loc: 1} |
| foo@a | {id:1, mail: "foo@a", loc: 1} |
| FoO@a | {id:1, mail: "foo@a", loc: 1} |
@Get("/user/{id}")
fun getUserById(id) {
user = cache.get(id)
if (user == null) {
user = repository.getUserById(id);
cache.put(id, user)
}
return user;
}
@Get("/user-by-mail/{mail}")
fun getUserByMail(mail) {
user = cache.get(mail)
if (user == null) {
user = repository.getUserByMail(mail);
cache.put(mail, user)
}
return user;
}
// repository code
fun getUserByMail(mail) {
mail = mail.lower();
//... some sql query
return user;
}fun onUserUpdate(user) {
cache.delete(user.id)
cache.delete(user.mail)
// how to invalidate all the permutations of
// the mail ?
}| key | value |
|---|---|
| 1 | {id:1, mail: "foo@a", loc: 1} |
| 2 | {id:2, mail: "bar@a", loc: 1} |
| foo@a | {id:1, mail: "foo@a", loc: 1} |
| FoO@a | {id:1, mail: "foo@a", loc: 1} |
| key | value |
|---|---|
| 1 | {id:1, slug: "foo", loc: {id: 1, name : "Rome"}} |
@Get("/user/{id}")
fun getUserById(id) {
user = cache.get(id)
if (user == null) {
user = repository.getUserById(id);
cache.put(id, user)
}
return user;
}If we rename "Rome" to "Roma"
we need to invalidate all the users from Rome
fun onLocationUpdate(location) {
// find all users with that location and invalidate
users = usersApi.findAllWithLocationId(location.id)
for (user in users) {
cache.delete(user.id)
cache.delete(user.mail)
cache.delete("L" + user.loc)
// more to come as the product grows
}
// find all trups with that location and invalidate
// find all the bookigns with that location and invalidate
// ....more
}| key | value |
|---|---|
| 1 | {id:1, slug: "foo", loc: {id: 1, name : "Rome"}} |
| key | value |
|---|---|
| L1 | [{id:1, slug: "foo", loc: 1}, {id:2, slug: "bar", loc: 1} ] |
| L2 | [] |
@Get("/users-by-location/{id}")
fun getUsersByLocation(id) {
users = cache.get("L" + id)
if (user == null) {
users = repository.getUsersByLocation(id)
cache.put("L" + id, users)
}
return users;
}
fun onUserUpdate(user) {
cache.delete(user.id)
cache.delete(user.mail)
// works only when user is added to collection
cache.delete("L" + user.loc)
// what is user changes location ? (we need the previous user state...)
}| key | value |
|---|---|
| L1 | [{id:1, slug: "foo", loc: 1}, {id:2, slug: "bar", loc: 1} ] |
| L2 | [] |
Common in CDNs
| key | value |
|---|---|
| 1 | ... |
| 2 | ... |
| foo | ... |
| FoO | ... |
| key | value |
|---|---|
| 1 | ... |
| 2 | ... |
| foo | ... |
| FoO | ... |
| key | tag |
|---|---|
| 1 | id-1 |
| key | tag |
|---|---|
| 2 | id-2 |
| ... |
| key | tag |
|---|---|
| foo | id-1 |
| key | tag |
|---|---|
| FoO | id-1 |
@Get("/user/{id}")
fun getUserById(id) {
user = cache.get(id)
if (user == null) {
user = repository.getUserById(id)
cache.put(id, user, getUserTags(user))
}
return user;
}
fun getUserTags(user) {
return ["id-" + user.id];
}| key | value | tags |
|---|---|---|
| 1 | ... | id-1 |
| 2 | ... | id-2 |
fun onUserUpdate(user) {
cache.deleteByTag("id-" + user.id)
}| key | value | tags |
|---|---|---|
| 1 | ... | id-1 |
| 2 | ... | id-2 |
@Get("/user/{id}")
fun getUserById(id) {
user = cache.get(id)
if (user == null) {
user = repository.getUserById(id)
cache.put(id, user, getUserTags(user))
}
return user;
}
@Get("/user-by-mail/{mail}")
fun getUserByMail(mail) {
user = cache.get(mail)
if (user == null) {
user = repository.getUserByMail(mail)
cache.put(mail, user, getUserTags(user))
}
return user;
}
fun getUserTags(user) {
return ["id-" + user.id];
}| key | value | tags |
|---|---|---|
| 1 | ... | id-1 |
| 2 | ... | id-2 |
| foo@a | ... | id-1 |
| FoO@a | ... | id-1 |
fun onUserUpdate(user) {
cache.deleteByTag("id-" + user.id)
}| key | value | tags |
|---|---|---|
| 1 | ... | id-1 |
| 2 | ... | id-2 |
| foo | ... | id-1 |
| FoO | ... | id-1 |
| key | value | tags |
|---|---|---|
| 1 | {id:1, slug: "foo", loc: {id: 1, name : "Rome"}} | id-1, loc-1 |
| 2 | {id:2, slug: "bar", loc: {id: 1, name : "Rome"}} | id-2, loc-1 |
@Get("/user/{id}")
fun getUserById(id) {
user = cache.get(id)
if (user == null) {
user = repository.getUserById(id);
cache.put(id, user, getUserTags(user))
}
return user;
}
fun getUserTags(user) {
return ["id-" + user.id, "loc-" + user.loc.id];
}fun onLocationUpdate(location) {
cache.deleteByTag("loc-" + location.id)
}| key | value | tags |
|---|---|---|
| 1 | {id:1, slug: "foo", loc: {id: 1, name : "Rome"}} | id-1, loc-1 |
| 2 | {id:2, slug: "bar", loc: {id: 1, name : "Rome"}} | id-2, loc-1 |
| key | value | tag |
|---|---|---|
| L1 | [{id:1, slug: "foo"}, {id:2, slug: "bar"} ] |
loc-1, id-1, id-2 |
| L2 | [] | loc-2 |
@Get("/users-by-location/{id}")
fun getUsersByLocation(id) {
users = cache.get("L" + id)
if (user == null) {
users = repository.getUsersByLocation(id)
cache.put("L" + id, users, ["loc-" + id] + getUsersTags(users));
}
return users;
}
fun getUsersTags(users) {
tags = [];
for (user in users) {
tags.concat(getUserTags(user))
}
return tags;
}
fun getUserTags(user) {
return ["id-" + user.id, "loc-" + user.loc.id];
}| key | value | tag |
|---|---|---|
| L1 | [{id:1, slug: "foo"}, {id:2, slug: "bar"} ] |
loc-1, id-1, id-2 |
| L2 | [] | loc-2 |
fun onUserUpdate(user) {
cache.deleteByTag("id-" + user.id)
cache.deleteByTag("loc-" + user.loc)
// id-1 will invalidate if the user changes location
// loc- will invalidate a user is being added or removed for that location
}Before (19 Sept 2024)
Lines of code: 4104
Coverage: ~77%
NOW
Lines of code: 3796
Coverage: ~83%
7% less code
(but we have more features)
cloc src/main
https://github.com/AlDanial/cloc
code to cache articles by tags (-90%)
Before (19 Sept 2024)
~$50-60/day
0,00000342 $/req
NOW
~$40-42/day
0,00000247 $/req
about -20%
Overall (-90%)
composer require symfony/cacheuse Symfony\Contracts\Cache\ItemInterface;
$beta = 1.0;
$value = $cache->get('my_cache_key', function (ItemInterface $item): string {
// ... do some HTTP request or heavy computations
$computedValue = 'foo';
$item->expiresAfter(3600);
$item->tag(['tag_0', 'tag_1']);
return $computedValue;
}, $beta);
// cache invalidation
$cache->invalidateTags(['tag_1', 'tag_3']);composer require symfony/cacheuse Symfony\Contracts\Cache\ItemInterface;
$beta = 1.0;
$value = $cache->get('my_cache_key', function (ItemInterface $item): string {
// ... do some HTTP request or heavy computations
$computedValue = 'foo';
$item->expiresAfter(3600);
$item->tag(['tag_0', 'tag_1']);
return $computedValue;
}, $beta);
// cache invalidation
$cache->invalidateTags(['tag_1', 'tag_3']);Random
Cache refresh
Cache expire
Cache set (60s)
time
Cache expire
Random
Cache refresh
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=604800
X-Cache-Tags: tag1,tag2
// ...bodyGET /user/123 HTTP/1.1PURGE / HTTP/1.1
Host: example.com
X-Xkey-Purge: tag1, tag3