nicolasff / webdis

A Redis HTTP interface with JSON output
https://webd.is
BSD 2-Clause "Simplified" License
2.82k stars 307 forks source link

transaction support #230

Open DeoLeung opened 1 year ago

DeoLeung commented 1 year ago

Hi,

I see transaction support in TODO for a while, is there any plan for that?

thanks :)

nicolasff commented 1 year ago

Hi @DeoLeung,

I originally left out transaction support in Webdis because I wasn't sure exactly how to implement it, specifically what the best API would be. Most Webdis requests likely use a single command, doing things like /GET/foo for example.

"Transactions" in Redis use the MULTI/EXEC keywords which simply wrap a list of commands that are all executed at once on Redis' single processing thread. This doesn't really work with the Webdis request structure: I don't think it would make much sense to send things like /MULTI/SET/foo/bar/.../EXEC. Among other issues, commands with a variable number of parameters just couldn't be expressed this way.

Another approach I had considered was to send them as a JSON list of commands in a POST request, e.g. sending this as the request body:

[
  ["MULTI"],
  ["SET", "foo", "1"],
  ["SET", "bar", "2"],
  ["EXEC"]
]

The downside being that you'd have to structure the commands in a special format, which is not something that Webdis does anywhere else; the only "structured data" used in Webdis request is in its responses, where a request to /SET/hello/world returns something like {"SET":[true,"OK"]}.

There is an alternative though, and I know from user reports and questions that people do run MULTI/EXEC transactions this way: you can store your transaction as a Lua script and then ask Redis (via Webdis) to run it. In fact this seems to be the recommended approach from the Redis project, as their documentation suggests that users execute scripts rather that building MULTI/EXEC transactions:

Something else to consider for transaction like operations in redis are redis scripts which are transactional. Everything you can do with a Redis Transaction, you can also do with a script, and usually the script will be both simpler and faster.

Here's an example where Webdis is used to execute the same "transaction" as above that's mutating both foo and bar atomically.

The Lua script can simply be:

redis.call('SET', 'foo', '1');
redis.call('SET', 'bar', '2');

So we can remove spaces and put it all on one line, then encode the quotes and add it after /SCRIPT/LOAD/. Let's preview what that would look like:

$ echo -n 'http://127.0.0.1:7379/SCRIPT/LOAD/'"redis.call('SET','foo','1');redis.call('SET','bar','2');" | sed -e "s/'/%27/g"
http://127.0.0.1:7379/SCRIPT/LOAD/redis.call(%27SET%27,%27foo%27,%271%27);redis.call(%27SET%27,%27bar%27,%272%27);

If you're building this command from a programming language, you just need to URL-encode the script, e.g.

> encodeURIComponent("redis.call('SET','foo','1');redis.call('SET','bar','2');")
"redis.call('SET'%2C'foo'%2C'1')%3Bredis.call('SET'%2C'bar'%2C'2')%3B"

Let's run our command by piping this encoded URL to xargs curl:

$ echo -n 'http://127.0.0.1:7379/SCRIPT/LOAD/'"redis.call('SET','foo','1');redis.call('SET','bar','2');" | sed -e "s/'/%27/g" | xargs curl
{"SCRIPT":"61202b13748238bf45d58fd21c8ad16831f7a4c1"}

This is our script hash; before running it let's first make sure foo and bar don't exist:

$ curl http://127.0.0.1:7379/DEL/foo/bar
{"DEL":0}

Then invoke the script with 0 parameters:

$ curl http://127.0.0.1:7379/EVALSHA/61202b13748238bf45d58fd21c8ad16831f7a4c1/0
{"EVALSHA":null}

And read back foo and bar:

$ curl http://127.0.0.1:7379/MGET/foo/bar
{"MGET":["1","2"]}

I hope the example helps! This should cover all use cases where MULTI/EXEC was previously required, and seems to be the way forward for multi-command operations as recommended by the Redis project. Thanks for bringing my attention to this note in the Webdis docs, I'll update them to mention Lua scripts instead.