1. Gambaran keseluruhan
Artikel ini adalah pengenalan kepada Lettuce, pelanggan Redis Java.
Redis adalah kedai nilai kunci dalam memori yang boleh digunakan sebagai pangkalan data, cache atau broker mesej. Data ditambahkan, ditanyakan, diubah, dan dihapus dengan perintah yang beroperasi pada kunci dalam struktur data dalam memori Redis.
Lettuce menyokong penggunaan komunikasi sinkron dan asinkron API Redis yang lengkap, termasuk struktur datanya, pemesejan pub / sub, dan sambungan pelayan dengan ketersediaan tinggi.
2. Mengapa Selada?
Kami telah membahas Jedis dalam salah satu catatan sebelumnya. Apa yang membuat Lettuce berbeza?
Perbezaan yang paling ketara adalah sokongan tidak segerak melalui antara muka Java 8's CompletionStage dan sokongan untuk Reactive Streams. Seperti yang akan kita lihat di bawah, Lettuce menawarkan antara muka semula jadi untuk membuat permintaan tak segerak dari pelayan pangkalan data Redis dan untuk membuat aliran.
Ia juga menggunakan Netty untuk berkomunikasi dengan pelayan. Ini menjadikan API "lebih berat", tetapi juga menjadikannya lebih sesuai untuk berkongsi sambungan dengan lebih dari satu utas.
3. Persediaan
3.1. Ketergantungan
Mari kita mulakan dengan menyatakan satu-satunya pergantungan yang kita perlukan dalam pom.xml :
io.lettuce lettuce-core 5.0.1.RELEASE
Versi perpustakaan terkini boleh diperiksa di repositori Github atau di Maven Central.
3.2. Pemasangan Redis
Kita perlu memasang dan menjalankan sekurang-kurangnya satu contoh Redis, dua jika kita ingin menguji mod pengelompokan atau sentinel (walaupun mod sentinel memerlukan tiga pelayan untuk berfungsi dengan betul.) Untuk artikel ini, kami menggunakan 4.0.x - yang versi stabil terkini pada masa ini.
Maklumat lebih lanjut mengenai memulakan dengan Redis boleh didapati di sini, termasuk muat turun untuk Linux dan MacOS.
Redis tidak menyokong Windows secara rasmi, tetapi ada port pelayan di sini. Kita juga boleh menjalankan Redis di Docker yang merupakan alternatif yang lebih baik untuk Windows 10 dan cara cepat untuk bangun dan berjalan.
4. Sambungan
4.1. Menyambung ke Pelayan
Menyambung ke Redis terdiri daripada empat langkah:
- Membuat URI Redis
- Menggunakan URI untuk menyambung ke RedisClient
- Membuka Sambungan Redis
- Menjana sekumpulan RedisCommands
Mari lihat pelaksanaannya:
RedisClient redisClient = RedisClient .create("redis://[email protected]:6379/"); StatefulRedisConnection connection = redisClient.connect();
A StatefulRedisConnection adalah apa yang ia kedengaran seperti; sambungan yang selamat untuk thread ke pelayan Redis yang akan mengekalkan sambungannya ke pelayan dan sambungkan semula jika diperlukan. Setelah kami mempunyai sambungan, kami dapat menggunakannya untuk melaksanakan perintah Redis sama ada secara serentak atau tidak segerak.
RedisClient menggunakan sumber sistem yang besar, kerana ia menyimpan sumber daya Netty untuk berkomunikasi dengan pelayan Redis. Aplikasi yang memerlukan pelbagai sambungan harus menggunakan satu RedisClient.
4.2. Rediskan URI
Kami membuat RedisClient dengan menyampaikan URI ke kaedah kilang statik.
Lettuce memanfaatkan sintaks khusus untuk Redis URI. Ini adalah skema:
redis :// [[email protected]] host [: port] [/ database] [? [timeout=timeout[d|h|m|s|ms|us|ns]] [&_database=database_]]
Terdapat empat skema URI:
- redis - pelayan Redis yang berdiri sendiri
- rediss - pelayan Redis yang tersendiri melalui sambungan SSL
- redis-socket - pelayan Redis yang berdiri sendiri melalui soket domain Unix
- redis-sentinel - pelayan Redis Sentinel
Contoh pangkalan data Redis dapat ditentukan sebagai bagian dari jalur URL atau sebagai parameter tambahan. Sekiranya kedua-duanya dibekalkan, parameter tersebut mempunyai keutamaan yang lebih tinggi.
Dalam contoh di atas, kami menggunakan representasi String . Lettuce juga mempunyai kelas RedisURI untuk membina sambungan. Ia menawarkan corak Pembina :
RedisURI.Builder .redis("localhost", 6379).auth("password") .database(1).build();
Dan pembina:
new RedisURI("localhost", 6379, 60, TimeUnit.SECONDS);
4.3. Perintah segerak
Mirip dengan Jedis, Lettuce menyediakan set perintah Redis lengkap dalam bentuk kaedah.
Walau bagaimanapun, Lettuce menerapkan kedua-dua versi segerak dan tidak segerak. Kami akan melihat versi segerak sebentar, dan kemudian menggunakan pelaksanaan tak segerak untuk sisa tutorial.
Setelah membuat sambungan, kami menggunakannya untuk membuat set arahan:
RedisCommands syncCommands = connection.sync();
Sekarang kita mempunyai antara muka intuitif untuk berkomunikasi dengan Redis.
Kami dapat menetapkan dan mendapatkan nilai String:
syncCommands.set("key", "Hello, Redis!"); String value = syncommands.get(“key”);
Kami boleh bekerja dengan hash:
syncCommands.hset("recordName", "FirstName", "John"); syncCommands.hset("recordName", "LastName", "Smith"); Map record = syncCommands.hgetall("recordName");
Kami akan membahas lebih banyak Redis kemudian dalam artikel.
Letchuce synchronous API menggunakan API tak segerak. Penyekatan dilakukan untuk kita di peringkat perintah. Ini bermakna lebih daripada satu pelanggan dapat berkongsi sambungan segerak.
4.4. Perintah tak segerak
Mari lihat arahan tak segerak:
RedisAsyncCommands asyncCommands = connection.async();
We retrieve a set of RedisAsyncCommands from the connection, similar to how we retrieved the synchronous set. These commands return a RedisFuture (which is a CompletableFuture internally):
RedisFuture result = asyncCommands.get("key");
A guide to working with a CompletableFuture can be found here.
4.5. Reactive API
Finally, let’s see how to work with non-blocking reactive API:
RedisStringReactiveCommands reactiveCommands = connection.reactive();
These commands return results wrapped in a Mono or a Flux from Project Reactor.
A guide to working with Project Reactor can be found here.
5. Redis Data Structures
We briefly looked at strings and hashes above, let's look at how Lettuce implements the rest of Redis' data structures. As we'd expect, each Redis command has a similarly-named method.
5.1. Lists
Lists are lists of Strings with the order of insertion preserved. Values are inserted or retrieved from either end:
asyncCommands.lpush("tasks", "firstTask"); asyncCommands.lpush("tasks", "secondTask"); RedisFuture redisFuture = asyncCommands.rpop("tasks"); String nextTask = redisFuture.get();
In this example, nextTask equals “firstTask“. Lpush pushes values to the head of the list, and then rpop pops values from the end of the list.
We can also pop elements from the other end:
asyncCommands.del("tasks"); asyncCommands.lpush("tasks", "firstTask"); asyncCommands.lpush("tasks", "secondTask"); redisFuture = asyncCommands.lpop("tasks"); String nextTask = redisFuture.get();
We start the second example by removing the list with del. Then we insert the same values again, but we use lpop to pop the values from the head of the list, so the nextTask holds “secondTask” text.
5.2. Sets
Redis Sets are unordered collections of Strings similar to Java Sets; there are no duplicate elements:
asyncCommands.sadd("pets", "dog"); asyncCommands.sadd("pets", "cat"); asyncCommands.sadd("pets", "cat"); RedisFuture
pets = asyncCommands.smembers("nicknames"); RedisFuture exists = asyncCommands.sismember("pets", "dog");
When we retrieve the Redis set as a Set, the size is two, since the duplicate “cat” was ignored. When we query Redis for the existence of “dog” with sismember, the response is true.
5.3. Hashes
We briefly looked at an example of hashes earlier. They are worth a quick explanation.
Redis Hashes are records with String fields and values. Each record also has a key in the primary index:
asyncCommands.hset("recordName", "FirstName", "John"); asyncCommands.hset("recordName", "LastName", "Smith"); RedisFuture lastName = syncCommands.hget("recordName", "LastName"); RedisFuture
We use hset to add fields to the hash, passing in the name of the hash, the name of the field, and a value.
Then, we retrieve an individual value with hget, the name of the record and the field. Finally, we fetch the entire record as a hash with hgetall.
5.4. Sorted Sets
Sorted Sets contains values and a rank, by which they are sorted. The rank is 64-bit floating point value.
Items are added with a rank, and retrieved in a range:
asyncCommands.zadd("sortedset", 1, "one"); asyncCommands.zadd("sortedset", 4, "zero"); asyncCommands.zadd("sortedset", 2, "two"); RedisFuture
valuesForward = asyncCommands.zrange(key, 0, 3); RedisFuture
valuesReverse = asyncCommands.zrevrange(key, 0, 3);
The second argument to zadd is a rank. We retrieve a range by rank with zrange for ascending order and zrevrange for descending.
We added “zero” with a rank of 4, so it will appear at the end of valuesForward and at the beginning of valuesReverse.
6. Transactions
Transactions allow the execution of a set of commands in a single atomic step. These commands are guaranteed to be executed in order and exclusively. Commands from another user won't be executed until the transaction finishes.
Either all commands are executed, or none of them are. Redis will not perform a rollback if one of them fails. Once exec() is called, all commands are executed in the order specified.
Let's look at an example:
asyncCommands.multi(); RedisFuture result1 = asyncCommands.set("key1", "value1"); RedisFuture result2 = asyncCommands.set("key2", "value2"); RedisFuture result3 = asyncCommands.set("key3", "value3"); RedisFuture execResult = asyncCommands.exec(); TransactionResult transactionResult = execResult.get(); String firstResult = transactionResult.get(0); String secondResult = transactionResult.get(0); String thirdResult = transactionResult.get(0);
The call to multi starts the transaction. When a transaction is started, the subsequent commands are not executed until exec() is called.
In synchronous mode, the commands return null. In asynchronous mode, the commands return RedisFuture . Exec returns a TransactionResult which contains a list of responses.
Since the RedisFutures also receive their results, asynchronous API clients receive the transaction result in two places.
7. Batching
Under normal conditions, Lettuce executes commands as soon as they are called by an API client.
This is what most normal applications want, especially if they rely on receiving command results serially.
However, this behavior isn't efficient if applications don't need results immediately or if large amounts of data are being uploaded in bulk.
Asynchronous applications can override this behavior:
commands.setAutoFlushCommands(false); List
futures = new ArrayList(); for (int i = 0; i < iterations; i++) { futures.add(commands.set("key-" + i, "value-" + i); } commands.flushCommands(); boolean result = LettuceFutures.awaitAll(5, TimeUnit.SECONDS, futures.toArray(new RedisFuture[0]));
With setAutoFlushCommands set to false, the application must call flushCommands manually. In this example, we queued multiple set command and then flushed the channel. AwaitAll waits for all of the RedisFutures to complete.
This state is set on a per connection basis and effects all threads that use the connection. This feature isn't applicable to synchronous commands.
8. Publish/Subscribe
Redis offers a simple publish/subscribe messaging system. Subscribers consume messages from channels with the subscribe command. Messages aren't persisted; they are only delivered to users when they are subscribed to a channel.
Redis uses the pub/sub system for notifications about the Redis dataset, giving clients the ability to receive events about keys being set, deleted, expired, etc.
See the documentation here for more details.
8.1. Subscriber
A RedisPubSubListener receives pub/sub messages. This interface defines several methods, but we'll just show the method for receiving messages here:
public class Listener implements RedisPubSubListener { @Override public void message(String channel, String message) { log.debug("Got {} on channel {}", message, channel); message = new String(s2); } }
We use the RedisClient to connect a pub/sub channel and install the listener:
StatefulRedisPubSubConnection connection = client.connectPubSub(); connection.addListener(new Listener()) RedisPubSubAsyncCommands async = connection.async(); async.subscribe("channel");
With a listener installed, we retrieve a set of RedisPubSubAsyncCommands and subscribe to a channel.
8.2. Publisher
Publishing is just a matter of connecting a Pub/Sub channel and retrieving the commands:
StatefulRedisPubSubConnection connection = client.connectPubSub(); RedisPubSubAsyncCommands async = connection.async(); async.publish("channel", "Hello, Redis!");
Publishing requires a channel and a message.
8.3. Reactive Subscriptions
Lettuce also offers a reactive interface for subscribing to pub/sub messages:
StatefulRedisPubSubConnection connection = client .connectPubSub(); RedisPubSubAsyncCommands reactive = connection .reactive(); reactive.observeChannels().subscribe(message -> { log.debug("Got {} on channel {}", message, channel); message = new String(s2); }); reactive.subscribe("channel").subscribe();
The Flux returned by observeChannels receives messages for all channels, but since this is a stream, filtering is easy to do.
9. High Availability
Redis offers several options for high availability and scalability. Complete understanding requires knowledge of Redis server configurations, but we'll go over a brief overview of how Lettuce supports them.
9.1. Master/Slave
Redis servers replicate themselves in a master/slave configuration. The master server sends the slave a stream of commands that replicate the master cache to the slave. Redis doesn't support bi-directional replication, so slaves are read-only.
Lettuce can connect to Master/Slave systems, query them for the topology, and then select slaves for reading operations, which can improve throughput:
RedisClient redisClient = RedisClient.create(); StatefulRedisMasterSlaveConnection connection = MasterSlave.connect(redisClient, new Utf8StringCodec(), RedisURI.create("redis://localhost")); connection.setReadFrom(ReadFrom.SLAVE);
9.2. Sentinel
Redis Sentinel monitors master and slave instances and orchestrates failovers to slaves in the event of a master failover.
Lettuce can connect to the Sentinel, use it to discover the address of the current master, and then return a connection to it.
To do this, we build a different RedisURI and connect our RedisClient with it:
RedisURI redisUri = RedisURI.Builder .sentinel("sentinelhost1", "clustername") .withSentinel("sentinelhost2").build(); RedisClient client = new RedisClient(redisUri); RedisConnection connection = client.connect();
We built the URI with the hostname (or address) of the first Sentinel and a cluster name, followed by a second sentinel address. When we connect to the Sentinel, Lettuce queries it about the topology and returns a connection to the current master server for us.
The complete documentation is available here.
9.3. Clusters
Redis Cluster uses a distributed configuration to provide high-availability and high-throughput.
Clusters shard keys across up to 1000 nodes, therefore transactions are not available in a cluster:
RedisURI redisUri = RedisURI.Builder.redis("localhost") .withPassword("authentication").build(); RedisClusterClient clusterClient = RedisClusterClient .create(rediUri); StatefulRedisClusterConnection connection = clusterClient.connect(); RedisAdvancedClusterCommands syncCommands = connection .sync();
RedisAdvancedClusterCommands holds the set of Redis commands supported by the cluster, routing them to the instance that holds the key.
A complete specification is available here.
10. Conclusion
In this tutorial, we looked at how to use Lettuce to connect and query a Redis server from within our application.
Lettuce supports the complete set of Redis features, with the bonus of a completely thread-safe asynchronous interface. It also makes extensive use of Java 8's CompletionStage interface to give applications fine-grained control over how they receive data.
Contoh kod, seperti biasa, boleh didapati di GitHub.