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.