Rumah > Java > javaTutorial > teks badan

Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?

Lepaskan: 2023-08-22 15:48:46
ke hadapan
704 orang telah melayarinya

Kunci yang diedarkan biasanya dilaksanakan dalam:

  • pangkalan data
  • cache (contohnya: Redis)

Dalam pembangunan sebenar, gunakan The yang paling biasa ialah Redis dan Zookeeper, jadi artikel ini hanya akan membincangkan kedua-dua perkara ini.

Sebelum membincangkan isu ini, mari kita lihat dahulu senario perniagaan:

Sistem A ialah sistem e-dagang, yang digunakan pada mesin pada masa ini Terdapat antara muka untuk pengguna membuat pesanan dalam sistem, tetapi pengguna mesti Anda perlu menyemak inventori untuk memastikan inventori mencukupi sebelum membuat pesanan untuk pengguna.

Redis中,用户下单的时候会更新RedisMemandangkan sistem mempunyai tahap keselarasan tertentu, inventori produk akan disimpan dalam inventori

terlebih dahulu.

Seni bina sistem pada masa ini adalah seperti berikut:
Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?

Tetapi ini akan menimbulkan masalah

: Jika pada masa tertentu, inventori produk tertentu dalam redis ialah 1, pada masa ini dua permintaan datang pada pada masa yang sama, satu daripadanya Selepas melaksanakan langkah 3 dalam rajah di atas, inventori dalam pangkalan data dikemas kini kepada 0, tetapi langkah 4 belum dilaksanakan lagi.

Permintaan lain mencapai langkah 2 dan mendapati inventori masih 1, jadi ia terus ke langkah 3.

Hasilnya terjual 2 item tapi sebenarnya stok ada 1 item sahaja.

Jelas sekali ada yang tidak kena! Ini ialah masalah terlebih jual inventori biasa

Pada ketika ini, kita boleh memikirkan penyelesaian dengan mudah: gunakan kunci untuk mengunci langkah 2, 3 dan 4, supaya selepas ia selesai, utas lain boleh masuk untuk melaksanakan langkah 2 . 🎜
Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?

Mengikut rajah di atas, apabila melaksanakan langkah 2, gunakan segerak atau ReentrantLock yang disediakan oleh Java untuk mengunci, dan kemudian lepaskan kunci selepas langkah 4 dilaksanakan.

Dengan cara ini, tiga langkah 2, 3 dan 4 "dikunci", dan berbilang benang hanya boleh dilaksanakan secara bersiri.

Tetapi masa-masa indah itu tidak bertahan lama, keselarasan keseluruhan sistem melonjak, dan satu mesin tidak dapat mengendalikannya lagi. Sekarang kita perlu menambah mesin, seperti yang ditunjukkan di bawah:

Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?

Selepas menambah mesin, sistem menjadi seperti yang ditunjukkan dalam gambar di atas, ya Tuhan!

Anggapkan bahawa permintaan daripada dua pengguna tiba pada masa yang sama, tetapi jatuh pada mesin yang berbeza Bolehkah kedua-dua permintaan ini dilaksanakan pada masa yang sama, atau adakah masalah terlebih jual inventori akan berlaku?

Kenapa? Oleh kerana dua sistem A dalam gambar di atas dijalankan dalam dua JVM berbeza, kunci yang mereka tambahkan hanya sah untuk urutan dalam JVM mereka sendiri dan tidak sah untuk urutan dalam JVM lain.

Jadi, masalahnya di sini ialah: mekanisme kunci asli yang disediakan oleh Java gagal dalam senario penggunaan berbilang mesin

Ini kerana kunci yang ditambahkan oleh kedua-dua mesin bukan kunci yang sama (dua kunci berada dalam JVM yang berbeza) .

Kemudian, selagi kami memastikan kunci yang ditambahkan pada kedua-dua mesin adalah sama, tidakkah masalah itu akan selesai?

Pada ketika ini, sudah tiba masanya untuk kunci yang diedarkan untuk membuat penampilan hebat mereka. Idea kunci yang diedarkan ialah:

Menyediakan global dan unik "benda" untuk memperoleh kunci dalam keseluruhan sistem, dan kemudian setiap sistem. boleh mengunci apabila perlu , semua meminta "benda" ini untuk mendapatkan kunci, supaya sistem yang berbeza boleh menganggapnya sebagai kunci yang sama.

Bagi "benda" ini, ia boleh menjadi Redis, Zookeeper, atau pangkalan data.

Penerangan teks tidak begitu intuitif, mari lihat gambar di bawah:

Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?

Melalui analisis di atas, kita tahu bahawa dalam kes senario inventori terlebih jual, menggunakan mekanisme penguncian asli Java tidak dapat menjamin keselamatan benang dalam pengedaran sistem penggunaan Jadi kita perlu menggunakan penyelesaian kunci yang diedarkan.

Jadi, bagaimana untuk melaksanakan kunci yang diedarkan? Kemudian bacalah!

Melaksanakan kunci yang diedarkan berdasarkan Redis

Analisis di atas mengapa kunci yang diedarkan harus digunakan.

Penyelesaian yang paling biasa ialah menggunakan Redis sebagai kunci yang diedarkan

Idea untuk menggunakan Redis untuk kunci yang diedarkan adalah lebih kurang begini: Tetapkan nilai dalam redis untuk menunjukkan bahawa kunci telah ditambahkan, dan kemudian padamkan kunci apabila kunci dilepaskan.

Kod khusus adalah seperti ini:

// 获取锁
// NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
SET anyLock unique_value NX PX 30000

// 释放锁:通过执行一段lua脚本
// 释放锁涉及到两条指令,这两条指令不是原子性的
// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
Salin selepas log masuk

Terdapat beberapa perkara penting dalam kaedah ini:

  • Pastikan anda menggunakan nilai kunci SET NX PX milisaat arahan

    Jika tidak, tetapkan nilai dahulu, dan kemudian tetapkan masa tamat tempoh, ini Ia bukan operasi atom Ia mungkin ranap sebelum menetapkan masa tamat, yang akan menyebabkan kebuntuan (kunci wujud secara kekal)

  • Nilai mestilah unik

    Ini untuk mengesahkan. bahawa nilai adalah dan apabila membuka kunci Kekunci akan dipadamkan hanya jika kunci adalah konsisten.

    Ini mengelakkan situasi: Katakan A memperoleh kunci dan masa tamat tempoh adalah 30s Selepas 35s, kunci telah dilepaskan secara automatik, tetapi B mungkin telah memperoleh kunci pada masa ini. Pelanggan A tidak boleh memadamkan kunci B.

Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?

Selain mempertimbangkan cara pelanggan melaksanakan kunci teragih, anda juga perlu mempertimbangkan penggunaan redis.

Redis mempunyai 3 kaedah penggunaan:

  • Mod mesin tunggal
  • tuan-hamba + mod pilihan raya sentinel
  • mod kluster redis

diuntungkan awak gunakan satu mesin Dalam mod penggunaan, akan ada satu titik masalah, selagi redis gagal. Menguncinya tidak akan berfungsi.

Menggunakan mod induk-hamba Apabila mengunci, hanya satu nod dikunci Walaupun ketersediaan tinggi dicapai melalui sentinel, jika nod induk gagal dan suis induk-hamba berlaku, masalah kehilangan kunci.

Berdasarkan pertimbangan di atas, sebenarnya, pengarang redis juga mempertimbangkan isu ini, dia mencadangkan algoritma RedLock secara kasarnya seperti ini:

Anggapkan bahawa mod penggunaan redis adalah kelompok redis. terdapat sejumlah 5 induk, dapatkan kunci melalui langkah berikut:

  • 获取当前时间戳,单位是毫秒
  • 轮流尝试在每个master节点上创建锁,过期时间设置较短,一般就几十毫秒
  • 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
  • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
  • 要是锁建立失败了,那么就依次删除这个锁
  • 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。

Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?

另一种方式:Redisson

此外,实现Redis的分布式锁,除了自己基于redis client原生api来实现之外,还可以使用开源框架:Redission

Redisson是一个企业级的开源Redis Client,也提供了分布式锁的支持。我也非常推荐大家使用,为什么呢?

回想一下上面说的,如果自己写代码来通过redis设置一个值,是通过下面这个命令设置的。

  • SET anyLock unique_value NX PX 30000

这里设置的超时时间是30s,假如我超过30s都还没有完成业务逻辑的情况下,key会过期,其他线程有可能会获取到锁。

这样一来的话,第一个线程还没执行完业务逻辑,第二个线程进来了也会出现线程安全问题。所以我们还需要额外的去维护这个过期时间,太麻烦了~

我们来看看redisson是怎么实现的?先感受一下使用redission的爽:

Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://192.168.31.101:7001")
.addNodeAddress("redis://192.168.31.101:7002")
.addNodeAddress("redis://192.168.31.101:7003")
.addNodeAddress("redis://192.168.31.102:7001")
.addNodeAddress("redis://192.168.31.102:7002")
.addNodeAddress("redis://192.168.31.102:7003");

RedissonClient redisson = Redisson.create(config);


RLock lock = redisson.getLock("anyLock");
lock.lock();
lock.unlock();
Salin selepas log masuk

就是这么简单,我们只需要通过它的api中的lock和unlock即可完成分布式锁,他帮我们考虑了很多细节:

  • redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行

  • redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?

    redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s

    这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。

  • redisson的“看门狗”逻辑保证了没有死锁发生。

    (如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁)

Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?

这里稍微贴出来其实现代码:

// 加锁逻辑
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 调用一段lua脚本,设置一些key、过期时间
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.addListener(new FutureListener<Long>() {
        @Override
        public void operationComplete(Future<Long> future) throws Exception {
            if (!future.isSuccess()) {
                return;
            }

            Long ttlRemaining = future.getNow();
            // lock acquired
            if (ttlRemaining == null) {
                // 看门狗逻辑
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}


<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);

    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              "if (redis.call(&#39;exists&#39;, KEYS[1]) == 0) then " +
                  "redis.call(&#39;hset&#39;, KEYS[1], ARGV[2], 1); " +
                  "redis.call(&#39;pexpire&#39;, KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "if (redis.call(&#39;hexists&#39;, KEYS[1], ARGV[2]) == 1) then " +
                  "redis.call(&#39;hincrby&#39;, KEYS[1], ARGV[2], 1); " +
                  "redis.call(&#39;pexpire&#39;, KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "return redis.call(&#39;pttl&#39;, KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}



// 看门狗最终会调用了这里
private void scheduleExpirationRenewal(final long threadId) {
    if (expirationRenewalMap.containsKey(getEntryName())) {
        return;
    }

    // 这个任务会延迟10s执行
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {

            // 这个操作会将key的过期时间重新设置为30s
            RFuture<Boolean> future = renewExpirationAsync(threadId);

            future.addListener(new FutureListener<Boolean>() {
                @Override
                public void operationComplete(Future<Boolean> future) throws Exception {
                    expirationRenewalMap.remove(getEntryName());
                    if (!future.isSuccess()) {
                        log.error("Can&#39;t update lock " + getName() + " expiration", future.cause());
                        return;
                    }

                    if (future.getNow()) {
                        // reschedule itself
                        // 通过递归调用本方法,无限循环延长过期时间
                        scheduleExpirationRenewal(threadId);
                    }
                }
            });
        }

    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    if (expirationRenewalMap.putIfAbsent(getEntryName(), new ExpirationEntry(threadId, task)) != null) {
        task.cancel();
    }
}
Salin selepas log masuk

另外,redisson还提供了对redlock算法的支持,

它的用法也很简单:

RedissonClient redisson = Redisson.create(config);
RLock lock1 = redisson.getFairLock("lock1");
RLock lock2 = redisson.getFairLock("lock2");
RLock lock3 = redisson.getFairLock("lock3");
RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3);
multiLock.lock();
multiLock.unlock();
Salin selepas log masuk

小结

本节分析了使用Redis作为分布式锁的具体落地方案,以及其一些局限性,然后介绍了一个Redis的客户端框架redisson。这也是我推荐大家使用的,比自己写代码实现会少care很多细节。

基于zookeeper实现分布式锁

常见的分布式锁实现方案里面,除了使用redis来实现之外,使用zookeeper也可以实现分布式锁。

在介绍zookeeper(下文用zk代替)实现分布式锁的机制之前,先粗略介绍一下zk是什么东西:

Zookeeper是一种提供配置管理、分布式协同以及命名的中心化服务。

zk的模型是这样的:zk包含一系列的节点,叫做znode,就好像文件系统一样每个znode表示一个目录,然后znode有一些特性:

  • Nod tertib: Jika pada masa ini terdapat nod induk /lock, kita boleh mencipta nod anak di bawah nod induk ini /lock,我们可以在这个父节点下面创建子节点;

    zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号

    也就是说,如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001

    zookeeper menyediakan ciri pesanan pilihan, contohnya, kita boleh mencipta nod anak "/lock/node-" dan nyatakan pesanan , kemudian zookeeper akan menambah nombor siri integer secara automatik berdasarkan bilangan nod anak semasa semasa menjana nod anak
  • Maksudnya, jika ia adalah nod anak pertama yang dicipta, maka nod anak yang dihasilkan akan menjadi /lock/node-0000000000, nod seterusnya ialah /lock/node-0000000001, dan sebagainya.
  • Nod sementara: Pelanggan boleh mencipta nod sementara Penjaga Zoo akan memadamkan nod secara automatik selepas sesi tamat atau tamat masa sesi.
  • Pemantauan acara: Apabila membaca data, kami boleh menetapkan pemantauan acara pada nod pada masa yang sama Apabila data atau struktur nod berubah, zookeeper akan memberitahu pelanggan. Pada masa ini, zookeeper mempunyai empat acara berikut:

    • Penciptaan nod
    • Pemadaman nod
    • Pengubahsuaian node

berubah

  1. Berdasarkan beberapa Disebabkan ciri-ciri zk, kita boleh dengan mudah membuat rancangan pelaksanaan menggunakan zk untuk melaksanakan kunci teragih:

  2. Menggunakan nod sementara dan nod tersusun zk, setiap utas yang memperoleh kunci bermakna mencipta pesanan sementara nod dalam zk. Contohnya, dalam direktori /lock/.

  3. Selepas berjaya mencipta nod, dapatkan semua nod sementara dalam direktori /lock, dan kemudian tentukan sama ada nod yang dicipta oleh benang semasa ialah nod dengan nombor siri terkecil daripada semua nod

  4. Jika nod dicipta oleh benang semasa Jika ia adalah nod dengan nombor urutan terkecil semua nod, ia dianggap bahawa pemerolehan kunci berjaya.

    🎜🎜🎜🎜Jika nod yang dicipta oleh benang semasa bukan nod dengan nombor siri terkecil daripada semua nod, tambahkan pendengar peristiwa pada nod sebelum nombor siri nod. 🎜

    比如当前线程获取到的节点序号为/lock/003,然后所有的节点列表为[/lock/001,/lock/002,/lock/003],则对/lock/002这个节点添加一个事件监听器。

如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断是否自己的节点序号是最小。

比如/lock/001释放了,/lock/002监听到时间,此时节点集合为[/lock/002,/lock/003],则/lock/002为最小序号节点,获取到锁。

整个过程如下:

Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?

具体的实现思路就是这样,至于代码怎么写,这里比较复杂就不贴出来了。

Curator介绍

Curator是一个zookeeper的开源客户端,也提供了分布式锁的实现。

他的使用方式也比较简单:

InterProcessMutex interProcessMutex = new InterProcessMutex(client,"/anyLock");
interProcessMutex.acquire();
interProcessMutex.release();
Salin selepas log masuk

其实现分布式锁的核心源码如下:

private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception
{
    boolean  haveTheLock = false;
    boolean  doDelete = false;
    try {
        if ( revocable.get() != null ) {
            client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
        }

        while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) {
            // 获取当前所有节点排序后的集合
            List<String>        children = getSortedChildren();
            // 获取当前节点的名称
            String              sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash
            // 判断当前节点是否是最小的节点
            PredicateResults    predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
            if ( predicateResults.getsTheLock() ) {
                // 获取到锁
                haveTheLock = true;
            } else {
                // 没获取到锁,对当前节点的上一个节点注册一个监听器
                String  previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
                synchronized(this){
                    Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);
                    if ( stat != null ){
                        if ( millisToWait != null ){
                            millisToWait -= (System.currentTimeMillis() - startMillis);
                            startMillis = System.currentTimeMillis();
                            if ( millisToWait <= 0 ){
                                doDelete = true;    // timed out - delete our node
                                break;
                            }
                            wait(millisToWait);
                        }else{
                            wait();
                        }
                    }
                }
                // else it may have been deleted (i.e. lock released). Try to acquire again
            }
        }
    }
    catch ( Exception e ) {
        doDelete = true;
        throw e;
    } finally{
        if ( doDelete ){
            deleteOurPath(ourPath);
        }
    }
    return haveTheLock;
}
Salin selepas log masuk

其实curator实现分布式锁的底层原理和上面分析的是差不多的。这里我们用一张图详细描述其原理:

Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?
图片

小结

本节介绍了Zookeeperr实现分布式锁的方案以及zk的开源客户端的基本使用,简要的介绍了其实现原理。

Perbandingan kelebihan dan kekurangan kedua-dua penyelesaian

Setelah mempelajari kedua-dua penyelesaian pelaksanaan kunci yang diedarkan, bahagian ini perlu membincangkan kelebihan dan kekurangan masing-masing penyelesaian pelaksanaan redis dan zk.

Untuk kunci redis yang diedarkan, ia mempunyai kelemahan berikut:

  • Cara untuk memperoleh kunci adalah mudah dan kasar Jika ia tidak dapat memperoleh kunci, ia akan terus mencuba untuk memperoleh kunci, yang memakan prestasi .
  • Selain itu, kedudukan reka bentuk redis menentukan bahawa datanya tidak konsisten dalam beberapa kes yang melampau, masalah mungkin berlaku. Model kunci tidak cukup teguh
  • Walaupun algoritma kunci merah digunakan untuk melaksanakannya, dalam beberapa senario yang rumit, tiada jaminan bahawa pelaksanaannya akan 100% bebas masalah Untuk perbincangan tentang kunci merah, lihat Bagaimana untuk melakukan penguncian teragih
  • edaran redis Untuk kunci, anda sebenarnya perlu sentiasa mencuba untuk mendapatkan kunci itu sendiri, yang menggunakan lebih banyak prestasi.

Tetapi sebaliknya, menggunakan redis untuk melaksanakan kunci teragih adalah sangat biasa dalam banyak perusahaan, dan dalam kebanyakan kes anda tidak akan menghadapi apa yang dipanggil "senario yang sangat kompleks"

Jadi menggunakan redis sebagai kunci teragih adalah bukan idea yang buruk Perkara yang paling penting tentang penyelesaian yang baik ialah redis mempunyai prestasi tinggi dan boleh menyokong pemerolehan konkurensi tinggi dan operasi kunci pelepasan.

Untuk kunci teragih zk:

  • Kedudukan reka bentuk semula jadi penjaga zoo ialah penyelarasan teragih dan konsistensi yang kukuh. Model kunci adalah teguh, mudah digunakan dan sesuai untuk kunci yang diedarkan.
  • Jika anda tidak dapat memperoleh kunci, anda hanya perlu menambah pendengar. Tidak perlu membuat tinjauan pendapat sepanjang masa, dan penggunaan prestasi adalah kecil.

Tetapi zk juga mempunyai kekurangannya: jika terdapat lebih ramai pelanggan yang kerap memohon kunci dan melepaskan kunci, tekanan pada kelompok zk akan menjadi lebih besar.

Ringkasan:

Ringkasnya, kedua-dua redis dan zookeeper mempunyai kelebihan dan kekurangan mereka. Kita boleh menggunakan isu ini sebagai faktor rujukan semasa membuat pemilihan teknologi.

Atas ialah kandungan terperinci Sekiranya saya menggunakan Redis atau Zookeeper untuk kunci yang diedarkan?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:Java后端技术全栈
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan