Mastering Two-Level Caching in Spring Boot with J2Cache: A Hands‑On Guide
This article introduces J2Cache, a two‑level caching framework for Spring Boot, explains its L1 (Caffeine/Ehcache) and L2 (Redis/Memcached) architecture, and provides a complete hands‑on example covering Maven dependencies, configuration, core service classes, REST controller, and testing procedures.
Environment: SpringBoot 2.7.12 + j2cache 2.8.5
1. Introduction
J2Cache is the two‑level cache framework currently used by OSChina (requires at least Java 8). The first‑level cache uses in‑memory storage (supporting Ehcache 2.x, Ehcache 3.x, and Caffeine), while the second‑level cache uses Redis (recommended) or Memcached. Because heavy cache reads can make the L2 network a bottleneck, L1 aims to reduce the number of L2 reads. The framework is mainly for clustered environments but can also be used in single‑node setups to avoid cold‑start impact after application restarts.
Data Reading
Read order → L1 → L2 → DB
Data update 1. Read the latest data from the database, then update L1 → L2 and broadcast a cache‑clear message. 2. Upon receiving the broadcast (manual clear & automatic L1 expiration), remove the specified cache entry from L1.
2. Practical Example
2.1 Dependency Management
<code><dependency></code>
<code> <groupId>net.oschina.j2cache</groupId></code>
<code> <artifactId>j2cache-core</artifactId></code>
<code> <version>2.8.5-release</version></code>
<code></dependency></code>
<code><dependency></code>
<code> <groupId>net.oschina.j2cache</groupId></code>
<code> <artifactId>j2cache-spring-boot2-starter</artifactId></code>
<code> <version>2.8.0-release</version></code>
<code></dependency></code>2.2 Configuration
<code>redis:</code>
<code> # address, multiple addresses separated by commas</code>
<code> hosts: localhost:6379</code>
<code> # database index</code>
<code> database: 11</code>
<code> # password</code>
<code> password: xxxooo</code>
<code> # connection timeout</code>
<code> timeout: 10s</code>
<code> # min idle connections in pool</code>
<code> min-idle: 0</code>
<code> # max idle connections in pool</code>
<code> max-idle: 8</code>
<code> # max active connections</code>
<code> max-active: 8</code>
<code> # max wait time (-1 means no limit)</code>
<code> max-wait: -1ms</code>
<code>---</code>
<code>j2cache:</code>
<code> openSpringCache: true</code>
<code> # cache null values</code>
<code> allowNullValues: true</code>
<code> redisClient: lettuce</code>
<code> l2CacheOpen: true</code>
<code> # L1 uses caffeine</code>
<code> L1:</code>
<code> provider_class: caffeine</code>
<code> L2:</code>
<code> # use SpringRedis for L2</code>
<code> provider_class: net.oschina.j2cache.cache.support.redis.SpringRedisProvider</code>
<code> config_section: redis</code>
<code> # use SpringRedis for broadcast notifications</code>
<code> broadcast: net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy</code>
<code># L1 cache configuration (caffeine)</code>
<code>caffeine:</code>
<code> # region and expiration time</code>
<code> region.xj: 10000, 120s</code>
<code>---</code>
<code>spring:</code>
<code> cache:</code>
<code> # use caffeine as L1 cache</code>
<code> type: caffeine</code>2.3 Core Service Class
<code>@Service</code>
<code>public class UserService {</code>
<code> private final UserRepository userRepository;</code>
<code> public UserService(UserRepository userRepository) {</code>
<code> this.userRepository = userRepository;</code>
<code> }</code>
<code> @Transactional</code>
<code> public User save(User user) {</code>
<code> return this.userRepository.saveAndFlush(user);</code>
<code> }</code>
<code> @Cacheable(value = {"xj"}, key = "#id")</code>
<code> public User get(Long id) {</code>
<code> return this.userRepository.findById(id).orElse(null);</code>
<code> }</code>
<code> @Transactional</code>
<code> @CacheEvict(value = {"xj"}, key = "#id")</code>
<code> public void remove(Long id) {</code>
<code> this.userRepository.deleteById(id);</code>
<code> }</code>
<code>}</code>2.4 Controller Interface
<code>@RestController</code>
<code>@RequestMapping("/users")</code>
<code>public class UserController {</code>
<code> private final UserService userService;</code>
<code> // Use CacheChannel to operate J2Cache</code>
<code> private final CacheChannel cacheChannel;</code>
<code> public UserController(UserService userService, CacheChannel cacheChannel) {</code>
<code> this.userService = userService;</code>
<code> this.cacheChannel = cacheChannel;</code>
<code> }</code>
<code> @GetMapping("/save")</code>
<code> public User save() {</code>
<code> User user = new User();</code>
<code> int num = new Random().nextInt(80);</code>
<code> user.setAge(num);</code>
<code> user.setName("姓名 - " + num);</code>
<code> user.setSex(num >= 50 ? "男" : "女");</code>
<code> return this.userService.save(user);</code>
<code> }</code>
<code> @GetMapping("/{id}")</code>
<code> public Object get(@PathVariable("id") Long id) {</code>
<code> // Retrieve from specified region and key; if not in L1/L2, fetch via Function</code>
<code> return this.cacheChannel.get("xj", id.toString(), key -> this.userService.get(id), true);</code>
<code> }</code>
<code> @GetMapping("/delete/{id}")</code>
<code> public Object remove(@PathVariable("id") Long id) {</code>
<code> this.userService.remove(id);</code>
<code> return "success";</code>
<code> }</code>
<code>}</code>2.5 Testing
First add data via the /save endpoint.
Query data with id=2 .
Level = 3 indicates the data was not in cache and was fetched from the database. After refreshing the page:
Level = 2 shows the data was retrieved from the Redis second‑level cache. Refresh again:
Level = 1 indicates the data came from the Caffeine first‑level cache; subsequent refreshes will continue to hit L1 as long as the entry has not expired.
Testing a non‑existent record shows a cache entry for a null object.
Testing deletion removes the entry from the cache immediately.
All content above completes the tutorial.
Finished!!!
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.