杰瑞科技汇

Java如何高效使用memcached?

  1. 环境准备:安装 Memcached 服务器和 Java 客户端库。
  2. Java 客户端选择:介绍最流行的客户端库 Xmemcached
  3. 核心操作:连接、增删改查、过期时间等。
  4. 高级特性:CAS(Check-And-Set)、计数器、分布式锁等。
  5. 完整示例代码

环境准备

1 安装 Memcached 服务器

如果你还没有 Memcached 服务器,可以按照以下步骤安装(以 Linux 为例):

Java如何高效使用memcached?-图1
(图片来源网络,侵删)
# 1. 安装 Memcached
sudo apt-get update
sudo apt-get install memcached
# 2. 启动 Memcached 服务
# -d: 以守护进程方式运行
# -m: 分配给 Memcached 的内存大小(MB),这里分配 512MB
# -p: 监听的端口,默认是 11211
# -u: 运行 Memcached 的用户
sudo memcached -d -m 512 -p 11211 -u root

你的 Memcached 服务器已经在 0.0.1:11211 上运行了。

2 准备 Java 开发环境

确保你已经安装了 JDK 和 Maven(或 Gradle),我们将使用 Maven 来管理项目依赖。


Java 客户端选择

在 Java 生态中,有几个成熟的 Memcached 客户端库,其中最推荐的是 Xmemcached

  • Xmemcached
    • 优点:性能极高,基于 NIO,支持连接池,功能全面,社区活跃,是目前的主流选择。
    • 维护者:来自中国的淘宝团队,对中文文档和社区支持较好。
  • SpyMemcached
    • 优点:老牌客户端,非常稳定。
    • 缺点:基于传统的阻塞 I/O,性能相比 Xmemcached 稍差。

我们选择 Xmemcached 进行后续讲解。

Java如何高效使用memcached?-图2
(图片来源网络,侵删)

创建 Maven 项目并添加依赖

在你的 pom.xml 文件中添加 Xmemcached 的依赖:

<dependencies>
    <!-- Xmemcached 客户端 -->
    <dependency>
        <groupId>com.googlecode.xmemcached</groupId>
        <artifactId>xmemcached</artifactId>
        <version>2.4.7</version> <!-- 建议使用最新版本 -->
    </dependency>
    <!-- 为了方便演示,添加一个日志实现 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.36</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.11</version>
    </dependency>
</dependencies>

核心操作详解

1 建立连接

Xmemcached 使用 MemcachedClient 类作为客户端核心,最佳实践是使用 MemcachedClientBuilder 来构建客户端,因为它可以方便地配置连接池、超时等参数。

import net.rubyeye.xmemcached.MemcachedClient;
import net.rubyeye.xmemcached.MemcachedClientBuilder;
import net.rubyeye.xmemcached.XMemcachedClientBuilder;
import net.rubyeye.xmemcached.auth.AuthInfo;
import net.rubyeye.xmemcached.utils.AddrUtil;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeoutException;
public class MemcachedConnection {
    public static void main(String[] args) {
        MemcachedClient memcachedClient = null;
        try {
            // 1. 创建 MemcachedClientBuilder
            // AddrUtil.getAddresses("host1:port1 host2:port2 ...") 用于配置多个 Memcached 节点
            MemcachedClientBuilder builder = new XMemcachedClientBuilder(
                    AddrUtil.getAddresses("127.0.0.1:11211")
            );
            // 2. 可选:设置连接池大小
            builder.setConnectionPoolSize(10);
            // 3. 可选:设置超时时间(毫秒)
            builder.setOpTimeout(1000); // 操作超时
            builder.setConnectTimeout(2000); // 连接超时
            // 4. 构建并启动客户端
            memcachedClient = builder.build();
            // 5. 测试连接是否成功
            System.out.println("连接成功!");
        } catch (IOException e) {
            System.err.println("创建 Memcached 客户端失败: " + e.getMessage());
        } finally {
            // 6. 关闭客户端,释放资源
            if (memcachedClient != null) {
                try {
                    memcachedClient.shutdown();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2 基本 CRUD 操作

Xmemcached 的 API 设计得非常简洁,所有操作都是异步的,返回一个 Future 对象,但也可以通过 get() 方法同步等待结果。

// 假设 memcachedClient 已经成功创建
// 1. 设置值 (set)
// set(key, expiration, value)
// expiration: 过期时间,单位是秒,0 表示永不过期。
try {
    memcachedClient.set("user:1001", 3600, "张三");
    System.out.println("设置键 'user:1001' 成功");
} catch (Exception e) {
    e.printStackTrace();
}
// 2. 获取值 (get)
try {
    String value = memcachedClient.get("user:1001");
    System.out.println("获取键 'user:1001' 的值: " + value);
} catch (Exception e) {
    e.printStackTrace();
}
// 3. 添加值 (add)
// 只有当键不存在时才添加,如果键已存在则操作失败
try {
    // 第一次添加成功
    boolean added = memcachedClient.add("session:abc123", 1800, "user_data_123");
    System.out.println("添加 'session:abc123' 是否成功: " + added);
    // 第二次添加会失败,因为键已存在
    added = memcachedClient.add("session:abc123", 1800, "new_data");
    System.out.println("再次添加 'session:abc123' 是否成功: " + added);
} catch (Exception e) {
    e.printStackTrace();
}
// 4. 替换值 (replace)
// 只有当键存在时才替换,如果键不存在则操作失败
try {
    boolean replaced = memcachedClient.replace("user:1001", 3600, "李四");
    System.out.println("替换 'user:1001' 是否成功: " + replaced);
} catch (Exception e) {
    e.printStackTrace();
}
// 5. 删除值 (delete)
try {
    memcachedClient.delete("user:1001");
    System.out.println("删除键 'user:1001' 成功");
} catch (Exception e) {
    e.printStackTrace();
}
// 再次获取,验证是否已删除
try {
    String value = memcachedClient.get("user:1001");
    System.out.println("删除后获取 'user:1001' 的值: " + value); // 输出 null
} catch (Exception e) {
    e.printStackTrace();
}

高级特性

1 CAS (Check-And-Set) 乐观锁

CAS 是 Memcached 的重要特性,用于防止并发更新导致的数据覆盖问题,它通过一个唯一的 cas 值来实现。

Java如何高效使用memcached?-图3
(图片来源网络,侵删)

流程

  1. gets(key) 获取值和其对应的 cas 值。
  2. 在客户端修改值。
  3. cas(key, expiration, newValue, casValue) 尝试用新值更新,只有当 casValue 与服务器端的当前 cas 值匹配时,更新才会成功。
try {
    // 1. 初始化一个计数器
    memcachedClient.set("counter:page_view", 0, 100L);
    // 2. 获取初始值和 CAS 值
    CASValue<Long> casValue = memcachedClient.gets("counter:page_view");
    long currentValue = casValue.getValue();
    long currentCas = casValue.getCas();
    System.out.println("当前计数器值: " + currentValue + ", CAS: " + currentCas);
    // 3. 模拟并发更新:增加 1
    long newValue = currentValue + 1;
    // 4. 尝试使用 CAS 更新
    boolean casResult = memcachedClient.cas("counter:page_view", 0, newValue, currentCas);
    System.out.println("CAS 更新是否成功: " + casResult);
    if (casResult) {
        System.out.println("更新成功!新值为: " + newValue);
    } else {
        System.out.println("更新失败!可能是其他线程先更新了。");
    }
} catch (Exception e) {
    e.printStackTrace();
}

2 计数器

Xmemcached 提供了原子性的 incrdecr 方法,非常适合实现计数器。

try {
    // 1. 初始化一个计数器(如果不存在)
    memcachedClient.set("counter:downloads", 0, 1000L);
    // 2. 增加 5
    long newCount = memcachedClient.incr("counter:downloads", 5);
    System.out.println("增加 5 后,下载次数为: " + newCount);
    // 3. 减少 2
    newCount = memcachedClient.decr("counter:downloads", 2);
    System.out.println("减少 2 后,下载次数为: " + newCount);
} catch (Exception e) {
    e.printStackTrace();
}

3 分布式锁

虽然 Redis 是分布式锁的首选,但 Memcached 也可以通过 add 命令实现一个简单的分布式锁。

原理add 命令在键不存在时才能成功,当多个线程/服务同时尝试 add 一个锁键时,只有一个能成功,获得锁,其他未成功的线程需要不断重试(自旋锁)或等待,释放锁时,直接 delete 即可。

public class MemcachedLock {
    private final MemcachedClient memcachedClient;
    private final String lockKey;
    private final long lockTimeout; // 锁的超时时间,防止死锁
    public MemcachedLock(MemcachedClient client, String key, long timeout) {
        this.memcachedClient = client;
        this.lockKey = key;
        this.lockTimeout = timeout;
    }
    /**
     * 尝试获取锁
     * @return 是否成功获取锁
     */
    public boolean tryLock() {
        try {
            // add 命令只在键不存在时才成功
            return memcachedClient.add(lockKey, (int) (lockTimeout / 1000), "locked");
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 释放锁
     */
    public void unlock() {
        try {
            memcachedClient.delete(lockKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// 使用示例
// MemcachedClient client = ...; // 已创建的客户端
// MemcachedLock lock = new MemcachedLock(client, "resource_lock_123", 5000); // 5秒锁超时
//
// if (lock.tryLock()) {
//     System.out.println("成功获取锁,执行业务逻辑...");
//     try {
//         // 模拟耗时操作
//         Thread.sleep(3000);
//     } catch (InterruptedException e) {
//         e.printStackTrace();
//     }
//     lock.unlock(); // 释放锁
//     System.out.println("业务逻辑执行完毕,已释放锁。");
// } else {
//     System.out.println("获取锁失败,资源正忙。");
// }

完整示例代码

下面是一个整合了连接、基本操作和 CAS 的完整示例。

import net.rubyeye.xmemcached.MemcachedClient;
import net.rubyeye.xmemcached.MemcachedClientBuilder;
import net.rubyeye.xmemcached.XMemcachedClientBuilder;
import net.rubyeye.xmemcached.utils.AddrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class MemcachedExample {
    private static final Logger logger = LoggerFactory.getLogger(MemcachedExample.class);
    public static void main(String[] args) {
        MemcachedClient memcachedClient = null;
        try {
            // 1. 创建客户端
            MemcachedClientBuilder builder = new XMemcachedClientBuilder(
                    AddrUtil.getAddresses("127.0.0.1:11211")
            );
            builder.setConnectionPoolSize(5);
            memcachedClient = builder.build();
            logger.info("成功连接到 Memcached 服务器。");
            // 2. 基本操作
            basicOperations(memcachedClient);
            // 3. CAS 操作
            casOperation(memcachedClient);
        } catch (IOException e) {
            logger.error("创建或关闭 Memcached 客户端时发生 IO 异常", e);
        } finally {
            if (memcachedClient != null) {
                try {
                    memcachedClient.shutdown();
                    logger.info("Memcached 客户端已关闭。");
                } catch (Exception e) {
                    logger.error("关闭 Memcached 客户端时发生异常", e);
                }
            }
        }
    }
    private static void basicOperations(MemcachedClient client) throws TimeoutException, InterruptedException, IOException {
        logger.info("--- 开始基本操作演示 ---");
        String key = "user:profile:101";
        String value = "Java Developer";
        // 设置
        client.set(key, 3600, value);
        logger.info("设置键 '{}': {}", key, value);
        // 获取
        String getValue = client.get(key);
        logger.info("获取键 '{}': {}", key, getValue);
        // 替换
        String newValue = "Senior Java Developer";
        client.replace(key, 3600, newValue);
        logger.info("替换键 '{}' 为: {}", key, newValue);
        getValue = client.get(key);
        logger.info("替换后获取键 '{}': {}", key, getValue);
        // 删除
        client.delete(key);
        logger.info("删除键 '{}'", key);
        getValue = client.get(key);
        logger.info("删除后获取键 '{}': {}", key, getValue); // 应为 null
        logger.info("--- 基本操作演示结束 ---\n");
    }
    private static void casOperation(MemcachedClient client) throws TimeoutException, InterruptedException, IOException {
        logger.info("--- 开始 CAS 操作演示 ---");
        String counterKey = "product:view_count:888";
        // 初始化计数器
        client.set(counterKey, 0, 100L);
        logger.info("初始化计数器 '{}': 100", counterKey);
        // 获取初始 CAS 值
        CASValue<Long> casValue = client.gets(counterKey);
        long currentValue = casValue.getValue();
        long currentCas = casValue.getCas();
        logger.info("当前计数器值: {}, CAS: {}", currentValue, currentCas);
        // 模拟并发更新:增加 1
        long newValue = currentValue + 1;
        logger.info("尝试将计数器从 {} 更新为 {}", currentValue, newValue);
        // 尝试 CAS 更新
        boolean casResult = client.cas(counterKey, 0, newValue, currentCas);
        if (casResult) {
            logger.info("CAS 更新成功!新值为: {}", newValue);
        } else {
            logger.info("CAS 更新失败!可能是其他线程先更新了。");
        }
        // 验证最终值
        Long finalValue = client.get(counterKey);
        logger.info("计数器 '{}' 的最终值为: {}", counterKey, finalValue);
        logger.info("--- CAS 操作演示结束 ---");
    }
}
  • 核心类MemcachedClient 是操作的核心,通过 MemcachedClientBuilder 进行配置和构建。
  • 基本操作set, get, add, replace, delete 方法非常直观。
  • 高级特性
    • CAS:通过 getscas 方法实现乐观锁,保证数据并发安全。
    • 计数器incr/decr 是原子操作,性能高。
    • 分布式锁:利用 add 的特性实现,简单但有效。
  • 最佳实践
    • 连接管理:使用单例模式管理 MemcachedClient,避免频繁创建和销毁。
    • 异常处理:妥善处理 TimeoutException, InterruptedException, IOException
    • 序列化Xmemcached 默认使用 Java 序列化,性能较差,对于高性能场景,建议配置 KryoFST 等高效的序列化方案。
    • 过期时间:合理设置过期时间,防止缓存雪崩和穿透。

希望这份详细的指南能帮助你顺利地在 Java 项目中集成和使用 Memcached!

分享:
扫描分享到社交APP
上一篇
下一篇