Posts Tagged ‘EhCache’

การทำ อาร์เอ็มไอ ดิสทรืบิวเท็ต แคช (RMI Distributed Cache) ด้วย อีเอชแคช

August 5th, 2009

จริงๆไม่ได้เกี่ยวกับสปริงตรงๆครับแต่เกี่ยวทางอ้อมๆ เพราะในกรณีที่เรานำแอพพลิเคชั่นของเราไปวางไว้บนคลัสเตอร์แล้วเราต้องการให้ข้อมูลที่อยู่ในโลคอลแคชของเราเหมือนกันทุกเราจะทำยังไง? แน่นอนปัญหาพวกนี้ไม่ได้เพิ่งเิกิดขึ้นกับเราคนแรก มันมักจะมีคนอื่นแก้ไว้ให้แล้วเสมอ ดังนั้นอย่าเทพ ไปแก้ปัญหาเหล่านี้ด้วยการเขียนเองทั้งหมด
หลังจากไล่อ่านเอกสารของอีเฮชแคชไปได้สามรอบผลคือรู้ว่ามันทำได้ห้าวิธีคือ อาร์เอ็มไอ(RMI),เจกรุ๊ป(JGroups),เจเอ็มเอส(JMS),เทอรราคอตต้า(Terracotta)และแคชเซิร์ฟเวอร์(Cache Server)
แต่เท่าที่ไล่ดูจากเอกสารแล้วที่พอจะเอาได้ไล่ออกจากเอกสารแย่ๆของอีเฮชแคชนั้น สรุปได้ว่าอาร์เอ็มไอ ดูเข้าท่าที่สุดเพราะเนื่อหาดูท่าจะอ่านรู้เรื่องที่สุดแต่หลังจากที่อ่านไปอีกสามรอบก็ ห่านนนนนนน อ่านไม่รู้เรื่องแต่โชคดีที่มีลิงค์เล็กที่ท้ายระบุว่าถ้าอยากชมการทำงานเรื่องของการเรพพลิเขตข้อมูลให้ไปดูที่ยูนิตเทสท์ ==”แต่ต้องยอมรับว่ายูนิตเทสท์เค้าเทพมากๆ ถึงแม้ว่าอ่านรอบแรกจะไม่รู้เรื่องเลยก็ตาม แต่หลังจากรื้อๆค้นแก้ไปแก้มาก็เห็นภาพชัดเจนโดยสามารถสรุปได้ดังนี้
สิ่งที่ต้องมีเพิ่มในคอนฟิกกูเรชั่นไฟล์
1.เพียร์โพรไวด์เดอร์ (PeerProvider)
2.แคชเมเนเจอร์เพียร์ลิสท์ซึนเนอร์ (CacheManagerPeerListener)
โดยเราสามารถเพิ่มลงไปได้ดังนี้

    <cacheManagerPeerProviderFactory
         class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
         properties="hostName=,
         peerDiscovery=automatic,
         multicastGroupAddress=230.0.0.1,
         multicastGroupPort=4446,
         timeToLive=64"/>

ส่วนของเพียร์โพรไวด์เดอร์มีหน้าที่ระบุว่าแต่ละแคชนั้นเป็นเพียร์ต่อกันไม่มีตัวใดตัวหนึ่งเป็นมาสเตอร์ และแต่ละเพียร์จะสื่อสารกันผ่านมัลติคาสท์กรุ๊ปแอดเดรส(MulticastGroupAddress)ด้วยการค้นหาแบบอัตโนมัติซึ่งเป็นวิธีที่ง่ายและสะดวกที่สุด ดังนั้นในกรณีของเราเราสามารถใส่ค่านี้ลงไปในคอนฟิกกูเรชั่นไฟล์ได้ทั้งสองเครื่องเหมือนๆกัน ส่วนต่อไปคือแคชเมเนเจอร์เพียร์ลิสท์ซึนเนอร์

    <cacheManagerPeerListenerFactory
         class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
         properties="hostName=,
         port=40001,
         remoteObjectPort=47000,
         socketTimeoutMillis="/>

แคชเมเนเจอร์เพียร์ลิสท์ซึนเนอร์ทำหน้าที่คอยฟังสัญญาณจากเพียร์ต่างๆและส่งต่อไปให้แคชเมเนเจอร์ตัวปัจจุบัน
ส่วนต่อไปคือเราต้องกำหนดการทำเรพพลิเคเตอร์ที่แคชเมเนเจอร์ของเราว่าต้องการให้ทำงานในโหมดไหน ในที่นี้เราจทำเป็นแบบซิงค์โครนัส และยอมรับการเปลี่ยนแปลงทุกชนืดที่เกิดขึ้นในแคชไม่ว่าจะเป็นการ แก้ไข ลบ หรือเพิ่ม ข้อมูล

    <cache name="getUserById"
           maxElementsInMemory="10"
           eternal="true"
           overflowToDisk="true">
        <cacheEventListenerFactory
           class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
           properties="replicateAsynchronously=false,
           replicatePuts=true,
           replicateUpdates=true,
           replicateUpdatesViaCopy=true,
           replicateRemovals=true "/>
        <bootstrapCacheLoaderFactory
           class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
    </cache>

ดังนั้นหลังจากที่เราทำการกำหนดค่าต่างๆไว้เรียบร้อยแล้วเราจะสามารถทดสอบการทำเรพพลิเขตข้อมูลข้ามแคชได้ด้วยการเขียนยูนิตเทสท์ดังนี้

       @Before
    public void setUp() throws Exception {

        MulticastKeepaliveHeartbeatSender.setHeartBeatInterval(1000);

        CountingCacheEventListener.resetCounters();
        manager1 = new CacheManager(AbstractCacheTest.TEST_CONFIG_DIR + "distribution/ehcache-distributed1.xml");
        manager2 = new CacheManager(AbstractCacheTest.TEST_CONFIG_DIR + "distribution/ehcache-distributed2.xml");

        //allow cluster to be established
        Thread.sleep(1020);

        cache1 = manager1.getCache(cacheName);
        cache1.removeAll();

        cache2 = manager2.getCache(cacheName);
        cache2.removeAll();

        //enable distributed removeAlls to finish
        waitForPropagate();

    }
    @Test
    public void testBigPutsProgagatesSynchronous() throws CacheException, InterruptedException {

        //Give everything a chance to startup
        StopWatch stopWatch = new StopWatch();
        Integer index;
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 1000; j++) {
                index = new Integer(((1000 * i) + j));
                manager2.getCache("sampleCache3").put(new Element(index,
                        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"));
            }

        }
        long elapsed = stopWatch.getElapsedTime();
        long putTime = ((elapsed / 1000));
        LOG.log(Level.INFO, "Put and Propagate Synchronously Elapsed time: " + putTime + " seconds");

        assertEquals(2000, manager1.getCache("getUserById").getSize());
        waitForPropagate();
        assertEquals(2000, manager2.getCache("getUserById").getSize());
    }

Cache ข้อมูลแบบเบสิคด้วย SpringAOP+EhCache

August 1st, 2009

ข้อมูลที่มักจะถูกใช้อยู่บ่อยๆ เป็นประจำ ใช้เยอะๆและข้อมูลเหล่านั้นมีขนาดใหญ่พอสมควรสำหรับผมตัวอย่างที่ยกง่ายๆเช่น Content หรือ สินค้าที่เป็นข้อมูลที่ถูกเรียกดูบ่อยๆในสิบอันดับแรก ถ้าเวบหรือระบบของเรา traffic ยังไม่สูงก็ไม่แปลกที่เราจะ SELECT ข้อมูลเหล่านั้นตรงๆจากระบบฐานข้อมูล แต่ในทางกลับกันในกรณีที่ระบบมี Traffic สูงมากการดึงข้อมูลแบบธรรมดาก็เปรียบเสมือนการทำลายระบบตัวเอง
ทางออกคือการใช้สิ่งเราเรียกว่า แคช(Cache) ซึ่งก็คือการเก็บขอ้มูลที่เราต้องใช้บ่อยๆไว้ในที่ๆที่ๆเราสามารถได้ผลออกมาเร็วๆเช่นเมมโมรี่และในจาวาเองก็มีเอพีไอสำหรับการทำแคชเยอะมากหลายหลากตัวไม่ว่าจะเป็น EhCache, JBossCache, JCS, OSCache, … แต่ละตัวก็จะมีข้อดีข้อเสียที่ต่างกันไป แต่สำหรับวันนี้เราจะมาใช้ EhCache กันเนื่องจากอันดับแรกมันสามารถรวมร่างกับสปริงได้สวยงามที่สุด สองใช้ง่ายๆ สามเอกสารห่วยแตกแต่ยูนิทเทส(UnitTest) ดีมากๆดีจนผมสามสารถเข้าใจการทำงานของมันได้โดยไม่ต้องเสียเวลาอ่านเอกสารห่วยๆของมัน
นอกจากนี้เราจะใช้งานมันคู่กับ สปริงเอโอพี(SpringAOP) โดยเราจะเขียน เมธอดอินเตอร์เซปเตอร์ เพื่อดักการทำงานของเมธอดของคลาสที่มีหน้าที่ดึงข้อมูลจากฐานข้อมูลเพราะเราไม่อยากเอางานเรื่องของการทำแคชชิ่งเข้าไปปนกับปกติของเมธอดเหล่านั้น
EhCache
ไมมีอะไรมากครับ เกริ่นนำก่อนตัวมันเองมีความสามารถมากมายแต่พื้นฐานที่สุดคือเราสามารถสร้างมันถังเก็บข้อมูล(Cache)กี่ถังก็ได้ผ่าน EhCacheManager โดยที่เราสามารถเก็บสิ่งของ(Element) ที่ประกอบไปด้วยคีย์(Key, Serialize Object) และข้อมูล(Object) ลงไปได้ดังนั้นสรุปขั้นตอนการใช้งาน EhCache ไว้ดังนี้สองขั้นตอนดังนี้
1. สร้างคอนฟิกกูเรชั่นไฟล์
2. รีจิสเตอร์บีนเข้าไปในสปริง
สร้าง ehcache.xml โดยพื้นฐานเลยเราสามารถง่ายๆได้ดังนี้

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <diskStore path="java.io.tmpdir" />
    <defaultCache maxElementsInMemory="10" eternal="false"
	timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" />
    <cache name="getNoPartTab" maxElementsInMemory="50" eternal="false"
      overflowToDisk="true" timeToIdleSeconds="0" timeToLiveSeconds="86400" />
</ehcache>

ไฟล์นี่จะมีสามส่วนด้วยกัน
“diskStore” ระบุว่าในกรณีที่ข้อมูลมีขนาดใหญ่เกินกว่าที่เรากำหนดไว้ให้เอาข้อมูลไปเก็บลงดิสก์
“defaultCache”(ต้องมี) กรณีที่เราพยายามเรียกใช้แคชที่ไม่มีคอนฟิกระบบจะทำการสร้างแคชให้เราตาม “defaultCache”
“cache name=…” เป็นแคชที่เราต้องการใช้งานเราโดยจะใช้ name เป็นคีย์ในการดึงแคชออกมาจากแคชเมเนเจอร์
(ส่วนที่เหลือไปอ่านในเอกสารเองนะครับ ไม่งั้นคงต้องเขียนเป็นเล่มๆ)

สร้าง บีนในสปริงชื่อ appCacheManager โดยใช้ net.sf.ehcache.CacheManager พร้อมระบุตำแหน่งของ ehcache.xml
(จริงๆเพียงเท่านี้ก็เพียงพอแล้วใครจะเรียกใช้ก็ มาที่บีนตัวนี้ได้เลย เราสามารถสร้างแคชเมเนเจอร์กี่ตัวก็ได้ตามใจเราแต่แต่ละตัวต้องมีคอนฟิกกูเรชั่นไฟล์ที่ต่างกัน)

    <bean id="appCacheManager" class="net.sf.ehcache.CacheManager">
        <constructor-arg index="0" type="java.net.URL"
			value="classpath:ehcache.xml" />
    </bean>

เท่านี้ก็เรียบร้อยต่อไป เราก็ไปเขียนเมธอดอินเตอร์เซปเตอร์กัน เราจะอินเตอร์เซปเตอร์ทำงานเป็น อะราวด์แอดไวซ์(Around Advice) คือสามารถเข้าไปล้างการทำงานของเมธอดที่เรากำหนดได้ตั้งแต่ก่อนทำงาน ขณะทำงาน และหลังทำงานโดยการทำงานสามารถลำดับขั้นตอนได้ดังนี้
1.ก่อนที่ getNoPartTab จะทำงานนั้นแอดไวช์จะทำการดึุงเอาแคชออกมาก่อน
2.สร้างคีย์ที่จะใช้ดึง โดยจะเอามาจากพารามิเตอร์ตัวแรกที่เราส่งผ่านมาให้เมธอด
3.ในกรณีที่ยังไ่ม่มีค่าที่เราต้องการระบบจะปล่อยให้ เมธอดนั้นทำงานไปจนเกือบจบ โดยที่ก่อนจะส่งค่าออกไปนั้น แอดไวส์จะนำค่าที่ได้ใส่ลงไปใน อิลิเมนท์ และจัดเก็บลงไปในแคช

public class MethodCachingInterceptor implements MethodInterceptor {

    private CacheManager cacheManager;
    private final Log log = LogFactory.getLog(getClass());

    @Override
    public Object invoke(final MethodInvocation methodInvocation) throws Throwable {
        final String targetMethodName = methodInvocation.getMethod().getName();
        if (log.isInfoEnabled()) {
            log.info("Attempting cacheManager.getCache first run (no args lookup).");
        }
        //Initialte Cache by names it as targetMethod Name Eg. getNoTab
        Cache cache = cacheManager.getCache(targetMethodName);

        final Object[] methodArgs = methodInvocation.getArguments();
        StringBuffer cacheKey = new StringBuffer(methodArgs[0].toString());
        boolean methodInvocationProceed = false;
        boolean methodInvocationCache = false;

        Object methodReturn = null;

        final Element cacheElement = cache.get(cacheKey.toString());
        if (cacheElement == null) {
            if (log.isInfoEnabled()) {
                log.info("--->Cache Element Not Found");
            }
            methodInvocationProceed = true;
            methodInvocationCache = true;
        } else {
            if (log.isInfoEnabled()) {
                log.info("--->Cache Element Found");
            }
            methodReturn = cacheElement.getValue();
            if (log.isInfoEnabled()) {
                log.info("Using Cached Element for methodReturn.");
            }
            methodInvocationProceed = false;
        }

        if (methodInvocationProceed) {
            methodReturn = methodInvocation.proceed();
            if (methodInvocationCache) {
                final Element newCacheElement = new Element(cacheKey.toString(),
                        (Serializable) methodReturn);
                cache.put(newCacheElement);
                if (log.isInfoEnabled()) {
                    log.info("*----------Created new CacheElement entry and stored in cache.");
                }
            }
        }
        return methodReturn;
    }

เรียบร้อยครับ ไม่มีอะไรซับซ้อน หลังจากนั้นเราก็ไปกำหนดการทำงานของมันในคอนฟิกกูเรชั่นไฟล์ดังนี้

    <aop:config>
        <aop:pointcut id="getNPT"
			expression="execution(* *..JdbcNoPartTabDAO.getNoPartTab(..))" />
        <aop:advisor id="methodTimingAdvisor"
			advice-ref="methodTimingAdvice" pointcut-ref="getNPT" />
        <aop:advisor id="methodCachingAdvisor"
			advice-ref="methodCachingAdvice" pointcut-ref="getNPT" />
    </aop:config>

    <bean id="methodCachingAdvice" class="com.spring66.caching.interceptor.MethodCachingInterceptor">
        <property name="cacheManager" ref="appCacheManager" />
    </bean>

เพื่อความมันส์อีกนิดเรามาจับเวลากันว่า ก่อนและหลังการใช้แคชเวลาเป็นอย่างไรบ้างครับและแน่นอนเราจะทำการเขียนเมธอดอินเตอร์เซปเตอร์อีกตัว


public class MethodTimingInterceptor implements MethodInterceptor {

	private final Log log = LogFactory.getLog(getClass());

	public Object invoke(final MethodInvocation methodInvocation)
			throws Throwable {
		final StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		Object methodResult = null;
		try {
			methodResult = methodInvocation.proceed();
		} finally {
			stopWatch.stop();
		}
		final long millis = stopWatch.getTime();
		final BigDecimal seconds = new BigDecimal(millis).divide(new BigDecimal(
				DateUtils.MILLIS_PER_SECOND), BigDecimal.ROUND_HALF_UP);

		if (log.isInfoEnabled()) {
			log.info("Method Invocation ["
					+ methodInvocation.getThis().getClass().getName() + "."
					+ methodInvocation.getMethod().getName() + "] Total Time: " + seconds
					+ "(seconds) " + millis + "(millis)");
		}

		return methodResult;
	}

}

และแน่นอนเราต้องไปสร้างบีนในแอพพลิเคชั่นคอนเท็กส์

    <bean id="methodTimingAdvice" class="com.spring66.caching.interceptor.MethodTimingInterceptor" />

และเราก็เพิ่มแอสเป็คนี้เข้าไปในพอยท์คัทเดิมของเราทำให้เราได้หน้าตาสุดท้ายเป็นดังนี้

    <aop:config>
        <aop:pointcut id="getAllAtmsPointcut" expression="execution(* *..JdbcNoPartTabDAO.getNoPartTab(..))" />
        <aop:advisor id="methodTimingAdvisor" advice-ref="methodTimingAdvice" pointcut-ref="getAllAtmsPointcut" />
        <aop:advisor id="methodCachingAdvisor" advice-ref="methodCachingAdvice" pointcut-ref="getAllAtmsPointcut" />
    </aop:config>

เรียบร้อยครับ