Sunday 1 January 2023

Distributed Job Scheduler Using Redisson

Job Scheduling is one of the main components required for every kind of business unit.Supporting Job scheduling in a distributed and scalable environment is one of the challenging problems to solve. Redis provides multiple solutions for distributed and scalable systems with the help of unique data structures implementation. In this blog we will discuss how to implement a distributed Job Scheduler using Redis via Redisson with a working spring boot project example.

Redisson Job Scheduler in a Distributed mode

Redisson works something similar to Remote Method Invocation. It stores byte code of Runnable or Callable implementation class with other config details in Redis data structures. This helps to trigger job scheduling of Runnable/Callable class implementation in multiple instances of a microservice running in parallel.Let’s try to understand the concept with a working spring boot project code.

We just need to add below dependency in pom.xml

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.19.0</version>
</dependency>


To implement redisson, we have two important spring bean objects that are required to be instantiated.

  • RedissonClient
  • RScheduledExecutorService

RedissonClient helps us to provide RScheduledExecutorService object. It helps us to connect with different kinds of Redis infrastructure implementations like standalone, cluster, master-slave and sentinel servers.


RScheduledExecutorService helps us to create workers and Executors that will eventually run our scheduled job in a microservice instance.


Below is the implementation of these two configs

@Configuration
public class RedisConfig {

  // maximumPoolSize of ExecutorService
  private final int maxConcurrency = 5;

  // thread pool name of ExecutorService
  private final String threadPoolName = "scheduler-pool";

  // name of Redisson Scheduled Executor
  private final String executorName = "Distributed-Scheduler";

  @Bean(destroyMethod = "shutdown")
  public RedissonClient redisson() {
    final Config config = new Config();
    config.useSingleServer().setAddress("redis://localhost:6379");
    return Redisson.create(config);
  }

  @Bean(destroyMethod = "")
  public RScheduledExecutorService rScheduledExecutorService() {

    final CustomizableThreadFactory threadFactory = new CustomizableThreadFactory(threadPoolName);
    threadFactory.setDaemon(true);
    final ThreadPoolExecutor threadPoolExecutor =
        new ThreadPoolExecutor(
            maxConcurrency,
            maxConcurrency,
            5L,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue(),
            threadFactory);

    final WorkerOptions workerOptions =
        WorkerOptions.defaults()
            .workers(2)
            .taskTimeout(10, TimeUnit.SECONDS)
            .executorService(threadPoolExecutor);
    final RScheduledExecutorService executorService =
        redisson().getExecutorService(executorName, ExecutorOptions.defaults());
    executorService.registerWorkers(workerOptions);
    return executorService;
  }
}


You can autowired RScheduledExecutorService bean in your custom service class

@Autowired
private RScheduledExecutorService executorService;

Use below command to execute a Runnable implementation as a Scheduled Task

executorService.scheduleWithFixedDelay(new MyScheduledTask(), 0, 10, TimeUnit.SECONDS);

Runnable implementation example for scheduled task

public class MyScheduledTask implements Runnable, Serializable {

  private static final long serialVersionUID = 9157644650L;
  private static final Logger LOGGER = LogManager.getLogger(MyScheduledTask.class);

  @Override
  public void run() {
    LOGGER.info("MyScheduledTask executed at {}", LocalDateTime.now());
  }
}

Logs for 2 parallel service instances running with the same code are shown in image below.

Logs for 2 parallel instances executing same Task

You can see how Job scheduling of MyScheduledTask class is distributed on two service instances. Also observe on how threads are used for ThreadPoolExecutor with 5 max pool size.


Now let's take a look on what data structure Redis creates for above implementation.

127.0.0.1:6379> keys *
"{Distributed-Scheduler:org.redisson.executor.RemoteExecutorService}:counter"
"{Distributed-Scheduler:org.redisson.executor.RemoteExecutorService}:scheduler"
"{Distributed-Scheduler:org.redisson.executor.RemoteExecutorService}:retry-interval"
"{Distributed-Scheduler:org.redisson.executor.RemoteExecutorService}:tasks"

Please note that Distributed-Scheduler which was taken as executorName in our code example is used as prefix for every Redis key.


counter : Count for number of jobs/tasks running

Redis command to see counter data : get key


scheduler : Sorted set which stores task-id

Redis command to see scheduler data : zrange key 0 -1


retry-interval : Time interval in milliseconds after which another one attempt to send Redis command will be executed

Redis command to see retry-interval data : get key


tasks : Storing hash for every sorted set key with serialised form of Runnable/Callable class implementation

Redis command to see tasks data : hgetall key


Full implementation of above code is available here


For more understanding about redisson read this


For more redisson configuration changes read this

No comments:

Post a Comment