|
23 | 23 | <P> |
24 | 24 | This project provides functionality for using a local cache for Cloudant databases with the |
25 | 25 | java-cloudant cloudant-client. |
| 26 | + By using client-side caching, data (that can be cached without violating consistency |
| 27 | + constraints) can be served directly from the client, eliminating the need to fetch the data from |
| 28 | + the server. This can considerably reduce the latency for data accesses. |
26 | 29 | </P> |
27 | 30 |
|
| 31 | +<h1>Basics</h1> |
| 32 | + |
28 | 33 | <P> |
29 | 34 | Caches implement the {@link com.cloudant.client.cache.Cache} interface (or one of its |
30 | 35 | sub-interfaces). An example implementation is given in |
31 | 36 | {@link com.cloudant.client.cache.LRUCache}, but the |
32 | | - <a href="http://www.javadoc.io/doc/com.cloudant/cloudant-client-cache-in-process/"> |
| 37 | + <a href="http://www.javadoc.io/doc/com.cloudant/cloudant-client-cache-in-process/" |
| 38 | + target="_blank"> |
33 | 39 | cloudant-client-cache-in-process</a> and |
34 | | - <a href="http://www.javadoc.io/doc/com.cloudant/cloudant-client-cache-redis/"> |
| 40 | + <a href="http://www.javadoc.io/doc/com.cloudant/cloudant-client-cache-redis/" target="_blank"> |
35 | 41 | cloudant-client-cache-redis</a> projects provide alternative implementations. |
36 | 42 | </P> |
37 | 43 |
|
38 | 44 | <P> |
39 | | - {@link com.cloudant.client.cache.DatabaseCache} and its sub-classes implement the java-cloudant |
40 | | - client's {@link com.cloudant.client.api.Database} interface, preferentially using the cache and |
| 45 | + {@link com.cloudant.client.cache.DatabaseCache} and its sub-classes extend the java-cloudant |
| 46 | + client's {@link com.cloudant.client.api.Database} class, preferentially using the cache and |
41 | 47 | then delegating to the remote database. |
42 | 48 | </P> |
| 49 | +<pre> |
| 50 | + {@code |
| 51 | + // Instantiate a cache, in this example an LRUCache with a maximum capacity of 100 objects |
| 52 | + Cache<String, Object> cache = new LRUCache<>(100); |
| 53 | + |
| 54 | + // Get the Cloudant client instance and database object for the desired database. |
| 55 | + CloudantClient client = ClientBuilder.account("example").build(); |
| 56 | + Database db = client.database("example-database", false); |
| 57 | + |
| 58 | + // Create a new DatabaseCache with the cache and your com.cloudant.client.api.Database |
| 59 | + // instance. |
| 60 | + Database cachedDb = new DatabaseCache(db, cache); |
| 61 | + // Use this cachedDb instance in place of your normal db instance to utilise the cache. |
| 62 | + // It may be worth keeping references to both the cached and un-cached instances if you want to |
| 63 | + // switch between cached and un-cached access to the database. |
| 64 | + |
| 65 | + // Example 1: Get document with ID "abcdef" from the cache if available, or from the database if |
| 66 | + // not yet cached. |
| 67 | + MyDocument abc = cachedDb.find(MyDocument.class, "abcdef"); |
| 68 | + |
| 69 | + // Example 2: Use the original Database instance, db, to get document "abcdef" direct from the |
| 70 | + // remote database, bypassing the cache. |
| 71 | + MyDocument abc = db.find(MyDocument.class, "abcdef"); |
| 72 | + } |
| 73 | +</pre> |
| 74 | + |
| 75 | +<P> |
| 76 | + See the <a href="http://www.javadoc.io/doc/com.cloudant/cloudant-client/" target="_blank"> |
| 77 | + java-cloudant client javadoc</a> for more information about the client. |
| 78 | +</P> |
| 79 | + |
| 80 | +<h1>Cache types</h1> |
| 81 | + |
| 82 | +<UL> |
| 83 | + <LI><b>In-process cache</b> |
| 84 | + |
| 85 | + <P> |
| 86 | + In-process caches store data within the process corresponding to the application. That |
| 87 | + way, there is no interprocess communication required for storing the data. For these |
| 88 | + implementations of in-process caches, Java objects can directly be cached. Data |
| 89 | + serialization is not required. In order to reduce overhead when the object is cached, |
| 90 | + the object (or a reference to it) can be stored directly in the cache. This means that |
| 91 | + changes to the object from the application could affect changes to the cached object |
| 92 | + itself. In order to prevent the value of a cached object from being modified by changes |
| 93 | + to the object being made in the application, a copy of the object can be made before the |
| 94 | + object is cached. This results in overhead for copying the object. |
| 95 | + </P> |
| 96 | + <UL> |
| 97 | + <LI>Implementations</LI> |
| 98 | + <UL> |
| 99 | + <LI>{@link com.cloudant.client.cache.LRUCache}</LI> |
| 100 | + <LI> |
| 101 | + <a href="http://www.javadoc.io/doc/com.cloudant/cloudant-client-cache-in-process/" |
| 102 | + target="_blank"> |
| 103 | + cloudant-client-cache-in-process</a></LI> |
| 104 | + </UL> |
| 105 | + <LI>Advantages</LI> |
| 106 | + <UL> |
| 107 | + <LI>Extremely fast</LI> |
| 108 | + <LI>Cached objects do not have to be serialized</LI> |
| 109 | + </UL> |
| 110 | + <LI>Disadvantages</LI> |
| 111 | + <UL> |
| 112 | + <LI>Not shared by multiple clients</LI> |
| 113 | + </UL> |
| 114 | + </UL> |
| 115 | + </LI> |
| 116 | + <BR/> |
| 117 | + <LI><b>Remote process cache</b> |
| 118 | + |
| 119 | + <P> |
| 120 | + In this approach, the cache runs in one or more separate processes from the application. |
| 121 | + A remote process cache can run on a separate node from the application as well. There is |
| 122 | + some overhead for communication with a remote process cache. In addition, data often has |
| 123 | + to be serialized before being cached. However, remote process caches also have some |
| 124 | + advantages over in-process caches. A remote process cache can be shared by multiple |
| 125 | + clients, and this feature is often desirable. Remote process caches can often be scaled |
| 126 | + across multiple processes and nodes to handle high request rates and increase |
| 127 | + availability. |
| 128 | + </P> |
| 129 | + <UL> |
| 130 | + <LI>Implementations</LI> |
| 131 | + <UL> |
| 132 | + <LI><a href="http://www.javadoc.io/doc/com.cloudant/cloudant-client-cache-redis/" |
| 133 | + target="_blank"> |
| 134 | + cloudant-client-cache-redis</a> for use with a running <a |
| 135 | + href="http://redis.io" target="_blank"> |
| 136 | + Redis</a> server |
| 137 | + </LI> |
| 138 | + </UL> |
| 139 | + <LI>Advantages</LI> |
| 140 | + <UL> |
| 141 | + <LI>Multiple clients can share cache</LI> |
| 142 | + <LI>Cache can scale to many processes</LI> |
| 143 | + </UL> |
| 144 | + <LI>Disadvantages</LI> |
| 145 | + <UL> |
| 146 | + <LI>Overhead of interprocess communication</LI> |
| 147 | + <LI>Cached objects may need to be serialized</LI> |
| 148 | + </UL> |
| 149 | + </UL> |
| 150 | + </LI> |
| 151 | +</UL> |
| 152 | + |
| 153 | +<h1>Guidance on document revisions</h1> |
| 154 | + |
| 155 | +<P> |
| 156 | + The revision information from a cached object has the potential to be out-of-date or missing. |
| 157 | + The reasons for this are set out at length below, but it is preceded by the short version. |
| 158 | +</P> |
| 159 | + |
| 160 | +<h2>TL;DR version</h2> |
| 161 | + |
| 162 | +<P> |
| 163 | + At the simplest level the revision token ({@code _rev} field) on objects retrieved using |
| 164 | + {@link com.cloudant.client.cache.DatabaseCache#find(java.lang.Class, java.lang.String)} may be |
| 165 | + obsolete or non-existent because the server side object may have been updated or the object was |
| 166 | + cached with no revision information. |
| 167 | + To get a suitable revision token for an update or a delete it is generally better to use |
| 168 | + {@link com.cloudant.client.api.Database#find(java.lang.Class, java.lang.String)} to bypass the |
| 169 | + cache or perform a HTTP HEAD request to get the document revision, which is returned in the |
| 170 | + {@code ETag} header field, without retrieving the entire document, for example: |
| 171 | +</P> |
| 172 | +<pre> |
| 173 | + {@code |
| 174 | + client.executeRequest(Http.HEAD(new URL(db.getDBUri().toString() + "/" + id))) |
| 175 | + .getConnection().getHeaderField("ETag"); |
| 176 | + } |
| 177 | +</pre> |
| 178 | + |
| 179 | +<h2>Detailed version</h2> |
| 180 | + |
| 181 | +<P> |
| 182 | + Object instances are added to the cache unmodified. This behaviour is consistent with the |
| 183 | + <a href="http://static.javadoc.io/com.cloudant/cloudant-client/2.3.0/overview-summary.html#Document%20revisions" |
| 184 | + target="_blank">behaviour of the java-cloudant client</a>, which does not modify objects |
| 185 | + after a write to the database. Consequently an object instance created from the deserialization |
| 186 | + of a document retrieved from the database will not be equal to the object instance that was |
| 187 | + saved because the server has added a revision token. |
| 188 | + It is the user's responsibility to retrieve and store the revision token when it is necessary |
| 189 | + for the application, for example to perform an update or delete. |
| 190 | + It is possible to set the retrieved revision token on a cached object reference to bring the |
| 191 | + cached instance up-to-date with the server document at that point in time. |
| 192 | +</P> |
| 193 | +<P> |
| 194 | + Here are some examples using the java-cloudant client alone or with the cache and the resulting |
| 195 | + revision token expectations. |
| 196 | +</P> |
| 197 | +<pre> |
| 198 | + {@code |
| 199 | + // Using java-cloudant only. |
| 200 | + |
| 201 | + // Create a new Foo with an _id, but no revision. |
| 202 | + Foo f = new Foo(); |
| 203 | + // Save the foo in the Database. |
| 204 | + Response response = db.save(f); |
| 205 | + |
| 206 | + // This example assumes no further changes have been made to the database document. |
| 207 | + // Sometime later get the Foo from the database. |
| 208 | + Foo f2 = db.find(response.getId()); // Returns deserialized document from the database. |
| 209 | + |
| 210 | + f2.getRev().equals(response.getRev()); // True, f2 was deserialized from the database document |
| 211 | + f2.equals(f); // False, f has not been modified by the database write and has no revision set |
| 212 | + |
| 213 | + // Set revision on f |
| 214 | + f.setRev(response.getRev()); |
| 215 | + f2.equals(f); // True as now f has the same revision as f2 |
| 216 | + } |
| 217 | +</pre> |
| 218 | +<P> |
| 219 | + When using a java-cloudant-cache the attempt to retrieve an object can return the original |
| 220 | + object directly from the cache without deserializing a document from the database. |
| 221 | + This results in a behaviour difference from using java-cloudant alone. |
| 222 | +</P> |
| 223 | +<pre> |
| 224 | + {@code |
| 225 | + // Using a java-cloudant-cache. |
| 226 | + |
| 227 | + // Create a new Foo with an _id, but no revision. |
| 228 | + Foo f = new Foo(); |
| 229 | + // Save the foo in the Database. |
| 230 | + Response response = cachedDb.save(f); |
| 231 | + |
| 232 | + // This example assumes no further changes have been made to the database document. |
| 233 | + // Sometime later get the Foo from the cached database. |
| 234 | + Foo f2 = cachedDb.find(response.getId()); // Returns the cached object. |
| 235 | + |
| 236 | + f2.getRev().equals(response.getRev()); // False, f2 has no revision information |
| 237 | + f2.equals(f); // True, neither f or f2 have a revision set (note f == f2 is also True) |
| 238 | + |
| 239 | + // Set revision on f |
| 240 | + f.setRev(response.getRev()); // Also updates the instance of f in the cache (and hence f2) |
| 241 | + f2.equals(f); // True, as before because f == f2 and both objects now have revision information. |
| 242 | + } |
| 243 | +</pre> |
| 244 | +<P> |
| 245 | + This behaviour difference is well defined, but can be confusing, particularly if using a cache |
| 246 | + with lifetimes that may purge expired objects from the cache. Consider this scenario. |
| 247 | +</P> |
| 248 | +<pre> |
| 249 | + {@code |
| 250 | + // Using a java-cloudant-cache with lifetimes. |
| 251 | + |
| 252 | + // Create a new Foo with an _id, but no revision. |
| 253 | + Foo f = new Foo(); |
| 254 | + // Save the foo in the Database. |
| 255 | + Response response = lifetimeCachedDb.save(f); |
| 256 | + |
| 257 | + // This example assumes no further changes have been made to the database document. |
| 258 | + // Sometime later get the Foo from the cached database. |
| 259 | + Foo f2 = lifetimeCachedDb.find(response.getId()); // Returns the cached object. |
| 260 | + |
| 261 | + f2.getRev().equals(response.getRev()); // False, f2 has no revision information |
| 262 | + f2.equals(f); // True, neither f or f2 have a revision set (note f == f2 is also True) |
| 263 | + |
| 264 | + // Wait a while longer, until object expires in cache |
| 265 | + Foo f3 = lifetimeCachedDb.find(response.getId()); |
| 266 | + // Expired so cache miss and object f3 is now deserialized from remote document |
| 267 | + f3.getRev().equals(response.getRev()); // True |
| 268 | + f3.equals(f); // False, f has no revision set |
| 269 | + |
| 270 | + // Note that the object returned from the same method call |
| 271 | + // lifetimeCachedDb.find(response.getId()) has different behaviour at different times because of |
| 272 | + // cache expiry. |
| 273 | + } |
| 274 | +</pre> |
| 275 | +<P> |
| 276 | + Setting the revision information after a write can to some extent provide more consistent |
| 277 | + behaviour. |
| 278 | +</P> |
| 279 | +<pre> |
| 280 | + {@code |
| 281 | + // Using a java-cloudant-cache with lifetimes. |
| 282 | + |
| 283 | + // Create a new Foo with an _id, but no revision. |
| 284 | + Foo f = new Foo(); |
| 285 | + // Save the foo in the Database. |
| 286 | + Response response = lifetimeCachedDb.save(f); |
| 287 | + // Set the revision information |
| 288 | + f.setRev(response.getRev()); |
| 289 | + |
| 290 | + // This example assumes no further changes have been made to the database document. |
| 291 | + // Sometime later get the Foo from the cached database. |
| 292 | + Foo f2 = lifetimeCachedDb.find(response.getId()); // Returns the cached object. |
| 293 | + |
| 294 | + f2.getRev().equals(response.getRev()); // True |
| 295 | + f2.equals(f); // True, f == f2 |
43 | 296 |
|
| 297 | + // Wait a while longer, until object expires in cache |
| 298 | + Foo f3 = lifetimeCachedDb.find(response.getId()); |
| 299 | + // Expired so cache miss and object f3 is now deserialized from remote document |
| 300 | + f3.getRev().equals(response.getRev()); // True |
| 301 | + f3.equals(f); // True, f and f3 have the same revision information |
| 302 | + } |
| 303 | +</pre> |
44 | 304 | <P> |
45 | | - See the <a href="http://www.javadoc.io/doc/com.cloudant/cloudant-client/">java-cloudant client |
46 | | - javadoc</a> for more information about the client. |
| 305 | + However, all of these examples have considered only cases where the server side document has not |
| 306 | + changed. Depending on the application scenario documents on the server side might be updated |
| 307 | + from other sources. Since providing the latest revision information is a requirement for a |
| 308 | + successful update or delete operation some care must be taken. Generally an application should |
| 309 | + have appropriate mechanisms in place to handle the potential |
| 310 | + {@link com.cloudant.client.org.lightcouch.DocumentConflictException}s or to inspect the |
| 311 | + {@code error} field of each {@link com.cloudant.client.api.model.Response} for bulk operations. |
47 | 312 | </P> |
48 | 313 | </body> |
49 | 314 | </html> |
0 commit comments