loading...

Multi-Tenant Cache Store using Custom RedisCacheManager

sidhantasamantaray profile image iamsid ・3 min read

Idea behind this post is to demonstrate how easily tenant specific cache-store in a multi-tenant application can be built using Spring annotations,Custom RedisCacheManager.

Why Caching ?

There are multiple benefits of caching.One key benefit is to reduce database access thus making access to data much faster and less expensive.
For more details on Redis cache use case refer this.

Multi-Tenant Application Design

As per Wikipedia,Multitenancy refers to a software architecture in which a single instance of software runs on a server and serves multiple tenants.
Here we will follow shared database(all tenants share same database->schema->table) approach. We will use just one entity as follow.

@Entity
@Table(name = "customers")
@Getter
@Setter
@NoArgsConstructor
@DynamicInsert
@DynamicUpdate
@FilterDef(name = "tenantFilter", parameters = {@ParamDef(name = "tenantKey", type = "string")})
@Filter(name = "tenantFilter", condition = "tenant = :tenantKey")
public class Customer implements Serializable {
    @Id
    String id;
    String name;
    //Identifier to distinguish each tenant
    String tenant;
}

While invoking any APIs mandatory http header parameter
x-tenant-id will be supplied.
Alt Text

  • For demo purpose we are using simple tenantKey.
  • Recommended to use an alphanumeric id which is unique and difficult to guess(e.g. 64b2a7b330614cd8804bcd93b72a069e).
Enable Caching

spring boot provides simple annotation based configuration to enable caching(more details).For Starting Redis in Local System

docker run --name local-redis -p 6379:6379 -d redis

Now cache the result of below repository method.Just by adding @Cacheable

import java.util.List;

@Repository
public interface CustomerRepository extends JpaRepository<Customer, String> {
    @Override
    @Cacheable(value = "customers")
    List<Customer> findAll();
}

Implementation(No custom manager)

Alt Text

  • Loading data from database into cache store.
 [
   "customers"
 ]
  • 1-1 Mapping between database table to cache store.
    • Fetch data from cache, then filter it based on the supplied tenantId and return the response.
    • Additional custom processing logic.
  • Assume a scenario where few tenants didn't access the application for some hours, but due to above implementation data will be available in cache store.

Now Lets See The Implementation(With custom manager)

Alt Text

  • Idea is to prefix all the cache stores with the tenantId.
      [
         "tenant1_customers",
         "tenant2_customers",
         "tenant3_customers",
         "tenant4_customers",
         "tenant5_customers"
       ]

How Can we Implement This ???

CustomCacheManger Extending RedisCacheManager

@Slf4j
public class CustomCacheManager extends RedisCacheManager {
    public CustomCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
        RedisCacheManager.builder()
                .cacheWriter(cacheWriter)
                .cacheDefaults(defaultCacheConfiguration)
                .build();
    }

    /**
     * @param name
     * @return Prefix the cache store name with the TENANT KEY
     * For SUPER ADMIN no prefix applied
     */
    @Override
    public Cache getCache(String name) {
        log.info("Inside getCache:" + name);
        String tenantId = TenantContext.getTenant().get();
        if (tenantId.equals(Constants.SUPER_ADMIN_TENANT)) {
            return super.getCache(name);
        } else if (name.startsWith(tenantId)) {
            return super.getCache(name);
        }
        return super.getCache(tenantId + "_" + name);
    }

}

Configuration class to create CacheManger Bean

@Configuration
@EnableCaching
public class RedisConfigurationCustom {

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()))
                //Configure in Property file as per use case, hardcoded just for demo
                .entryTtl(Duration.ofSeconds(600));
    }

    @Bean
    public RedisCacheWriter redisCacheWriter() {

        return RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory());

    }

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {

        return new LettuceConnectionFactory();
    }

    @Bean
    public RedisCacheManager redisCacheManager() {
        return new CustomCacheManager(redisCacheWriter(), redisCacheConfiguration());
    }

Congratulations now the basic implementation is complete


Let's see what is going on

  • Using HandlerInterceptor , tenant value is fetched from Request Headers and set in ThreadLocal.
  • We have created a CustomCacheManager by extending RedisCacheManager,then override getCache(String name) implementation to manipulate the name.
  • As you can see we are storing the supplied TenantId in ThreadLocal ,hence we can use it inside getCache(),the one we are overriding to manipulate the cache name.
  • Anyplace where cache name(e.g. customers) is specified within @cachable getCache() method will be invoked and return name with tenantId prefixed to it for further use within the application.
  • For some use cases we might need to call the getCache method explicitly using the CustomCacheManager, hence to avoid multiple prefixing additional checks are used.
    • One such use case is having a scheduled process as super admin to clean cache stores for all the tenant.

This doesn't have all the answers you might want, but I hope this is a helpful starting point.

GitHub logo Sidhanta-Samantaray / multitenant-redis-caching

Multi-Tenant Cache Store using Custom RedisCacheManager

Getting Started

Start Redis in Local System

docker run --name local-redis -p 6379:6379 -d redis

To start the Application

image

Goal of Application

Demonstrate tenant specific cache-store in a multi-tenant application using Spring annotations and custom RedisCacheManager.

Idea is to prefix all applicable cache stores with the tenantId.

  [
     "tenant1_customers"
     "tenant2_customers"
     "tenant3_customers",
     "tenant4_customers",
     "tenant5_customers"
   ]

Use sample data for populating the database table

Intercepting Requests
@Configuration
@EnableWebMvc
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //All the APIs which requires tenant level isolation
        registry.addInterceptor(new TenantInterceptor())
                .addPathPatterns("/tenant/**"); 
        registry.addInterceptor(new AdminTenantInterceptor())
                .addPathPatterns("/admin/**");//For Super Admin
    }
}
Interceptor Usage for
…

Reference Documents

Discussion

pic
Editor guide