Showing posts with label spring data. Show all posts
Showing posts with label spring data. Show all posts

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.

Monday, December 15, 2014

Spring Data Dynamic Query

saya mau berbagi sedikit tentang dynamic query menggunakan spring data. dynamic query yang saya maksud adalah melakukan query dengan parameter yang dinamis menggunakan satu repository method.

saya punya domain class Customer sebagai berikut:
 @Entity  
 @Table(name = "POST")  
 public class Customer extends Base {  
   
   @Column(name = "name")  
   private String name;  
   
   @Column(name = "phone")  
   private String phone;  
   
   @Column(name = "address")  
   private String address;  
   
   @Column(name = "email")  
   private String email;  
   
   @Column(name = "visit_count")  
   private Integer visitCount;  
   
   @Column(name = "gender")  
   private String gender;  
   
   //SETTER GETTER  
 }  

misalnya ada kebutuhan untuk:
- query Customer by name
- query Customer by name AND phone
- query Customer by gender AND visitCount.

ada beberapa cara melakukan query di spring data, ada namedquery, ada method query, ada criteria query. sebagai referensi, saya rasa blog ini http://www.petrikainulainen.net/spring-data-jpa-tutorial/ bagus untuk memulai mempelajari spring data. untuk bisa menggunakan parameter yang dinamis, criteria query bisa kita gunakan.

spring data menyediakan interface org.springframework.data.jpa.repository.JpaSpecificationExecutor<T> untuk memfasilitasi criteria query. criteria akan dibangun dan dibungkus dalam org.springframework.data.jpa.domain.Specification<T>. maka yang harus kita lakukan adalah meng-extends interface JpaSpecificationExecutor di interface CustomerRepository kita, kemudian membungkus parameter ke dalam sebuah interface Specification.

ini CustomerRepository saya:
 @Repository  
 public interface CustomerRepository extends JpaRepository<Customer, Integer>, JpaSpecificationExecutor<Customer>{  

 }  

lalu seperti biasa, untuk mengakses level DAO, saya buat service interface:
 public interface CustomerService {  

      public void save(List<CustomerVO> custVos);  

      public List<CustomerVO> search(Map<String, Object> parameters);  

 }  

method save akan kita gunakan untuk menginput data Customer, dan method search akan kita gunakan sebagai pintu untuk melakukan berbagai query sesuai kebutuhan di atas. untuk menjaga fleksibilitas, saya gunakan argumen Map<String, Object> untuk search tersebut.

kira-kira seperti ini penggambaran cara kerjanya:

misalkan ada sebuah query "select * from customer where name like '%{name}%' and phone like '%{phone}%' and visit_count={count}"

maka yang dimaksud dengan predicate adalah:
- predicate1: name like '%{name}%'
- predicate2: phone like '%{phone}%'
- predicate3: visit_count={count}

CriteriaBuilder yang menyusun tiap Predicate tersebut dengan operator 'and', sehingga Specificationnya menjadi "{predicate1} and {predicate2} and {predicate3}"

kemudian spring data repository (JpaSpecificationExecutor) yang akan melengkapi "select * from customer where {specification}"

ini implementasi saya:
 @Service  
 public class CustomerServiceImpl implements CustomerService {  

      @Autowired  
      private CustomerRepository customerRepository;  

      @Autowired  
      private CustomerVoConverter customerVoConverter;  

      @Override  
      public void save(List<CustomerVO> custVos) {  
           List<Customer> customers = new ArrayList<Customer>();  
           for(CustomerVO custVo : custVos){  
                customers.add(customerVoConverter.transferVOToModel(custVo, new Customer()));  
           }  
           customerRepository.save(customers);  
      }  

      @Override  
      public List<CustomerVO> search(Map<String, Object> parameters) {  
           List<CustomerVO> vos = null;  
           List<Customer> customers = customerRepository.findAll(dynamicSearchParams(parameters));  
           if(!customers.isEmpty()){  
                vos = customerVoConverter.transferListOfModelToListOfVO(customers, new ArrayList<CustomerVO>());  
           }  
           return vos;  
      }  

      private Specification<Customer> dynamicSearchParams(final Map<String, Object> parameters) {  
     return new Specification<Customer>() {  

                @Override  
                public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {  
                     Predicate[] predicates = getPredicates(root, cb, parameters);  
                     return cb.and(predicates);  
                }  

                private Predicate[] getPredicates(Root<Customer> root, CriteriaBuilder cb, Map<String, Object> parameters){  
                     Predicate[] predicates = new Predicate[parameters.size()];  
                     Iterator<String> paramKeyIterator = parameters.keySet().iterator();  
                     int i = 0;  
                     while(paramKeyIterator.hasNext()){  
                          String key = paramKeyIterator.next();  
                          Object value = parameters.get(key);  
                          Predicate predicate = null;  
                          if(root.get(key).getJavaType() == String.class){  
                               predicate = cb.like(cb.upper(root.get(key).as(String.class)), "%" + ((String)value).toUpperCase() + "%");  
                          }else{  
                               predicate = cb.equal(root.get(key), value);  
                          }  
                          predicates[i] = predicate;  
                          i++;  
                     }  
                     return predicates;  
                }  
     };  
   }  
 }  

saya memisahkan model untuk entity dan model untuk business process, di business process saya gunakan model CustomerVO, dan CustomerVoConverter adalah class yang saya gunakan untuk meng-convert entity ke VO dan sebaliknya.

interface JpaSpecificationExecutor<T> memiliki method List<T> findAll(Specification<T> spec). method ini yang saya gunakan untuk mengimplementasi search function di atas.

 List<Customer> customers = customerRepository.findAll(dynamicSearchParams(parameters));  

kita lihat di dalam method dynamicSearchParams, yang direturn adalah implementasi Specification<Customer> yang dibangun dengan menggunakan CriteriaBuilder yang disusun dari beberapa Predicate (disimpan di dalam array), dengan operator 'and'. ini disusun di dalam method toPredicate bawaan dari interface Specification.

kemudian kita lihat method getPredicates, method ini yang membongkar isi Map parameters dan membangun Predicate untuk tiap map entry.

perhatikan bagian root.get(key), root adalah proyeksi dari object Customer dan get(key) adalah fungsi untuk memanggil field key yang ada di object Customer.

misalnya key = "name" maka root.get(key) akan memanggil value dari field 'name' yang ada di object Customer. ini artinya, tiap key di Map parameters harus sesuai dengan nama field entity Customer. jika salah, misalnya key yang didaftarkan adalah 'nama', maka ini exception yang akan dithrow
 org.springframework.dao.InvalidDataAccessApiUsageException: Unable to resolve attribute [nama] against path; nested exception is java.lang.IllegalArgumentException: Unable to resolve attribute [nama] against path  

di sini saya melakukan pemisahan untuk field yang typenya String, saya gunakan clause 'like' dan yang lain menggunakan 'equal' untuk predicatenya:
 if(root.get(key).getJavaType() == String.class){  
      predicate = cb.like(cb.upper(root.get(key).as(String.class)), "%" + ((String)value).toUpperCase() + "%");  
 }else{  
      predicate = cb.equal(root.get(key), value);  
 }  

sudah, begitu saja. sekarang kita coba dengan menggunakan unit test, saya pakai junit:

 @RunWith(SpringJUnit4ClassRunner.class)  
 @ContextConfiguration(locations = { "/spring/applicationContext.xml"})  
 public class CustomerServiceTest {  

      @Autowired  
      private CustomerService customerService;  

      @Autowired  
      private CustomerRepository customerRepository;  

      @Before  
      public void insertCustomers(){  
           List<CustomerVO> customers = new ArrayList<CustomerVO>();  
           for(int i=0; i<10; i++){  
                CustomerVO custVo = new CustomerVO();  
                custVo.setAddress("alamat " + i);  
                custVo.setEmail("cust" + i + getMail(i));  
                custVo.setGender(i%2 == 0 ? "M" : "F");  
                custVo.setName(getName(i));  
                custVo.setPhone(getPhone(i));  
                custVo.setVisitCount(i % 3);  
                customers.add(custVo);  
           }  
           customerService.save(customers);  
           List<Customer> custs = customerRepository.findAll();  
           Assert.assertTrue(custs.size() == 10);  
      }  

      private String getMail(int i){  
           String mail = null;  
           if(i%3 == 0){  
                mail = "@mail.com";  
           }else if(i%5 == 0){  
                mail = "@google.com";  
           }else if(i%7 == 0){  
                mail = "@yahoo.com";  
           }else{  
                mail = "@test.com";  
           }  
           return mail;  
      }  

      private String getName(int i){  
           String name = null;  
           if(i%2 == 0){  
                name = "eko " + i;  
           }else if(i%3 == 0){  
                name = "budi " + i;  
           }else if(i%7 == 0){  
                name = "nama " + i;  
           }else{  
                name = "customer " + i;  
           }  
           return name;  
      }  

      private String getPhone(int i){  
           String phone = "9999";  
           if(i%3 == 0){  
                phone = "0811" + i + phone;  
           }else if(i%4 == 0){  
                phone = "0856" + i + phone;  
           }else{  
                phone = "0878" + i + phone;  
           }  
           return phone;  
      }  

      @Test  
      public void findByName(){  
           Map<String, Object> parameters = new HashMap<String, Object>();  
           parameters.put("name", "budi");  
           System.out.println("--FIND BY NAME 'budi'--");  
           List<CustomerVO> custVos = customerService.search(parameters);  
           for(CustomerVO custVo : custVos){  
                System.out.println(custVo);  
           }  

           System.out.println("\n");  
           Map<String, Object> parameters2 = new HashMap<String, Object>();  
           parameters2.put("name", "eko");  
           parameters2.put("phone", "0856");  
           System.out.println("--FIND BY NAME 'eko' AND PHONE '0856'--");  
           List<CustomerVO> custVos2 = customerService.search(parameters2);  
           for(CustomerVO custVo : custVos2){  
                System.out.println(custVo);  
           }  

           System.out.println("\n");  
           Map<String, Object> parameters3 = new HashMap<String, Object>();  
           parameters3.put("gender", "f");  
           parameters3.put("visitCount", "1");  
           System.out.println("--FIND BY GENDER 'f' AND VISITCOUNT 1--");  
           List<CustomerVO> custVos3 = customerService.search(parameters3);  
           for(CustomerVO custVo : custVos3){  
                System.out.println(custVo);  
           }  
           System.out.println("\n");  
      }  
 }  

method getName, getMail, getPhone hanya untuk me-'random'-kan value saja.

dan outputnya adalah:
 --FIND BY NAME 'budi'--  
 CustomerVO [name=budi 3, phone=081139999, address=alamat 3, email=cust3@mail.com, visitCount=0, gender=F]  
 CustomerVO [name=budi 9, phone=081199999, address=alamat 9, email=cust9@mail.com, visitCount=0, gender=F]  

 --FIND BY NAME 'eko' AND PHONE '0856'--  
 CustomerVO [name=eko 4, phone=085649999, address=alamat 4, email=cust4@test.com, visitCount=1, gender=M]  
 CustomerVO [name=eko 8, phone=085689999, address=alamat 8, email=cust8@test.com, visitCount=2, gender=M]  

 --FIND BY GENDER 'f' AND VISITCOUNT 1--  
 CustomerVO [name=customer 1, phone=087819999, address=alamat 1, email=cust1@test.com, visitCount=1, gender=F]  
 CustomerVO [name=nama 7, phone=087879999, address=alamat 7, email=cust7@yahoo.com, visitCount=1, gender=F]  

berhasil, 1 method untuk berbagai parameter query.

kita tidak pernah tau kapan field di Customer akan bertambah, kita tidak pernah tau kapan dan apa saja kombinasi search parameter untuk Customer. semua itu ada kemungkinan berkembang seiring perjalanan business processnya. karena itu akan lebih baik jika dari awal kita sudah menyiapkan kemungkinan tersebut dengan menyediakan fungsi-fungsi yang dinamis.

thanks, semoga bermanfaat.