Friday, September 18, 2015

menguji atomisitas redis - INCR command

post ini ditujukan untuk menguji sifat atomisitas command INCR redis.

kebutuhannya adalah, misalnya di aplikasi saya membutuhkan sebuah generator kode unik. cara termudah untuk membuat kode unik adalah dengan melakukan increment yang dimulai dari suatu angka, dan seterusnya. nantinya ini bisa dikombinasikan dengan character lain.

jika kita melihat dari 1 jvm, hal ini mudah saja, misalnya kita bisa buat sebuah variable static yang menampung angka tertentu dan menyiapkan sebuah method synchronized bagi thread yang hendak mengambil value tersebut (dan meng-increment-nya).

kira-kira seperti ini gambarannya


namun hal ini akan menjadi masalah saat aplikasi kita semakin besar dan membutuhkan scaling ke lebih dari 1 jvm. karena static variable dan synchronized hanya berlaku di 1 jvm saja. anggaplah kita punya 2 jvm, jika menggunakan static variable dan synchronized method, maka masing-masing jvm akhirnya akan menyimpan angka incrementnya sendiri-sendiri.



sehingga jika digabungkan, misalnya kita hendak menyimpan value itu dalam sebuah database yang sama dimana field tersebut dibuat unique, besar kemungkinannya kita akan kena ConstraintViolationException.

lalu bagaimana solusinya?

idenya adalah kedua jvm tersebut harus mengakses memory yang sama. sebuah cache bisa kita manfaatkan, mari kita coba dengan redis. kita letakkan instance redis di sebuah tempat, bisa di vm yang sama atau berbeda, untuk pengujian ini semuanya berada di 1 vm yang sama. tetapi untuk production jelas sebaiknya masing-masing memiliki vm-nya sendiri-sendiri.

kira-kira seperti ini skema umumnya


dalam percobaan ini, kita akan menggunakan command INCR milik redis,
"The counter pattern is the most obvious thing you can do with Redis atomic increment operations. The idea is simply send an INCR command to Redis every time an operation occurs." -http://redis.io/commands/INCR

command ini bersifat atomic, sehingga aman untuk proses multithread.

skema ini yang akan saya coba


saya akan aktifkan 1 jvm dulu, berkomunikasi dengan redis, memanfaatkan command INCR untuk mendapatkan sebuah value, dan value tersebut akan saya simpan di database. penyimpanan di database ini hanya bertujuan untuk mempermudah pengecekan, nantinya saya akan cek dengan menggunakan query

 select * from (  
     select value, count(1) n from redis_result group by value  
 ) a where a.n>1;  

jika INCR berjalan dengan benar, maka query tersebut akan menghasilkan result kosong.

saya menggunakan springboot 1.2.5 dengan library spring data, orm hibernate, database connection pool menggunakan apache commons-dbcp 1.4, redis connection pool dengan jedis 2.5.2.

skenarionya, saya akan menyiapkan 1 controller untuk memudahkan akses (tadinya saya mau menggunakan junit tapi junit tidak memungkinkan untuk test multithread). controller tersebut akan memanggil suatu class service yang akan mengeksekusi 5 thread. di dalam masing-masing thread tersebut saya akan melakukan looping sebanyak 2000 iterasi yang akan memanggil command INCR ke redis dan menyimpan value-nya ke database (table redis_result).

dengan skenario ini, maka akan ada sebanyak 5*2000 record yang tersimpan di table.

berikut ini configurasi redis connection pool nya:
 <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">  
     <property name="testOnBorrow" value="true"/>  
     <property name="testOnReturn" value="true"/>  
     <property name="testWhileIdle" value="true"/>  
     <property name="numTestsPerEvictionRun" value="10"/>  
     <property name="maxTotal" value="5000" />  
     <property name="minIdle" value="100" />  
   </bean>  
   <!-- for pooling the publisher -->  
   <bean id="redisConnectionPool" class="redis.clients.jedis.JedisPool" destroy-method="destroy">  
     <constructor-arg index="0" ref="jedisPoolConfig"/>  
     <constructor-arg index="1" value="localhost" type="java.lang.String"/>  
     <constructor-arg index="2" value="6379" type="int"/>  
   </bean>  

configurasi db connection pool:
 <bean class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" id="dataSource">  
     <property name="driverClassName" value="${database.driverClassName}"/>  
     <property name="url" value="${database.url}"/>  
     <property name="username" value="${database.username}"/>  
     <property name="password" value="${database.password}"/>  
     <<property name="validationQuery" value="SELECT version();"/>  
     <property name="initialSize" value="100"/>  
     <property name="maxActive" value="200"/>  
     <property name="maxIdle" value="100">  
   </bean>  

RedisUtil.java, yaitu class yang berfungsi sebagai redis client
 @Component  
 public class RedisUtil {  

   private static final Logger LOGGER = LoggerFactory.getLogger(RedisUtil.class);  

   @Autowired  
   @Qualifier(value = "redisConnectionPool")  
   JedisPool redisConnectionPool;  

   private void releaseJedisConnection(Jedis jedis){  
     if(null != jedis) {  
       LOGGER.debug("Return redis connection to pool");  
       redisConnectionPool.returnResource(jedis);  
     }  
   }  

   /**  
    * INCR key  
    * @param key  
    * @return  
    */  
   public Long increment(String key){  
        Long result = null;  
        Jedis jedis = null;  
     try {  
          jedis = redisConnectionPool.getResource();  
       result = jedis.incr(key);  
     } catch (JedisConnectionException jce) {  
          LOGGER.error("Failed add set to {} :: {}", key, jce.getMessage());  
     } finally {  
          releaseJedisConnection(jedis);  
     }  
     return result;  
   }  

   /**  
    * SET key  
    * @param key  
    * @return  
    */  
   public void set(String key, String value){  
        Jedis jedis = null;  
     try {  
          jedis = redisConnectionPool.getResource();  
       jedis.set(key, value);  
     } catch (JedisConnectionException jce) {  
          LOGGER.error("Failed add set to {} :: {}", key, jce.getMessage());  
     } finally {  
          releaseJedisConnection(jedis);  
     }  
   }  
 }  

RedisTask.java, yaitu class yang meng-increment key di redis dan menyimpan di database (dalam skema di atas, ini sama dengan thread01, thread02, dst..)
 public class RedisTask implements Runnable {  
        
      RedisUtil redisUtil;  
      RedisResultRepository redisResultRepository;  
        
      private static final String INCR_KEY = "incrKey";  
        
      public RedisTask(){  
           redisUtil = ApplicationContextHolder.getApplicationContext().getBean(RedisUtil.class);  
           redisResultRepository = (RedisResultRepository) ApplicationContextHolder.getApplicationContext().getBean("redisResultRepository");  
      }  
        
      public String getIncrValue() {  
           String result = null;  
   
           Long val = redisUtil.increment(INCR_KEY);  
           if (val == null || val.intValue() == 0) {  
                redisUtil.set(INCR_KEY, 1 + "");  
                result = getIncrValue();  
           } else {  
                result = val.toString();  
           }  
   
           return result;  
      }  
   
      @Override  
      public void run() {  
           for(int i=0; i<2000; i++){  
                String value = getIncrValue();  
                RedisResult result = new RedisResult(value);  
                redisResultRepository.save(result);  
           }  
      }  
        
 }  

RedisDemoService.java, yaitu class yang akan mengeksekusi 5 instance RedisTask secara concurrent
 @Service  
 public class RedisDemoService {  
        
      private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(5);  
        
      public void redisIncrTest(){  
             
           for(int i=0; i<5; i++){  
                System.out.println("execute redis task " + i);  
                EXECUTOR.execute(new RedisTask());  
           }  
             
      }  
        
 }  

RedisResult.java, yaitu POJO model domainnya
 @Entity  
 public class RedisResult extends BaseEntity {  
        
      private static final long serialVersionUID = -4902968469441816467L;  
        
      @Column(name = "value")  
      private String value;  
        
      public RedisResult(){  
             
      }  
        
      public RedisResult(String value){  
           this.value = value;  
      }  
   
      public String getValue() {  
           return value;  
      }  
   
      public void setValue(String value) {  
           this.value = value;  
      }  
 }  

RedisResultRepository.java, yaitu interface dari springdata yang akan menjadi akses komunikasi ke db
 @Repository  
 public interface RedisResultRepository extends JpaRepository<RedisResult, Long> {  
   
 }  

dan yang terakhir adalah controller-nya untuk kita akses dengan REST, RedisDemoController.java
 @RestController  
 @RequestMapping(value = "/redis")  
 public class RedisDemoController {  
        
      @Autowired  
      private RedisDemoService redisDemoService;  
   
      @RequestMapping(value = "/demo", method = RequestMethod.GET)  
      @ResponseBody  
      public boolean calculate() throws Exception {  
   
           redisDemoService.redisIncrTest();  
   
           return true;  
      }  
        
 }  

jika aplikasi kita nyalakan pada port 8181, kemudian kita bisa akses dia dengan
 curl -XGET http://localhost:8181/redis/demo  

setelah prosesnya selesai, kita count apakah jumlahnya benar

lalu kita cek apakah ada duplikasi value


oke, aman. sejauh ini tidak ada masalah.
lalu kita harus coba dengan menggunakan 2 jvm, saya menggunakan code yang sama tetapi saya nyalakan pada port yang berbeda, aplikasi kedua ini menggunakan port 8182.

kita coba secara bersama-sama
 curl -XGET http://localhost:8181/redis/demo  
 curl -XGET http://localhost:8182/redis/demo  

kemudian kita cek apakah jumlah recordnya benar


apakah ada duplikasi


woohoo, hasilnya baik-baik saja.

jadi kesimpulannya adalah command INCR redis dapat digunakan untuk kasus seperti di atas. untuk lebih meyakinkan diri mungkin perlu dilakukan lagi dengan 10 atau lebih jvm, tentunya jika memiliki environment yang sesuai.

thanks.

No comments:

Post a Comment