Adapt virtual cache. (#32)

diff --git a/src/plugin/plugin_memcached.rs b/src/plugin/plugin_memcached.rs
index 969a756..3c0716c 100644
--- a/src/plugin/plugin_memcached.rs
+++ b/src/plugin/plugin_memcached.rs
@@ -13,106 +13,131 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+use std::{any::Any, collections::HashMap};
+
 use super::Plugin;
 use crate::{
     component::COMPONENT_PHP_MEMCACHED_ID,
     context::RequestContext,
+    exception_frame::ExceptionFrame,
     execute::{get_this_mut, AfterExecuteHook, BeforeExecuteHook},
+    tag::{CacheOp, TAG_CACHE_CMD, TAG_CACHE_KEY, TAG_CACHE_OP, TAG_CACHE_TYPE},
 };
-use anyhow::{bail, Context};
+use anyhow::Context;
 use once_cell::sync::Lazy;
-use phper::{functions::call, values::ExecuteData};
+use phper::{
+    objects::ZObj,
+    values::{ExecuteData, ZVal},
+};
 use skywalking::{skywalking_proto::v3::SpanLayer, trace::span::Span};
-use tracing::{debug, warn};
+use tracing::{debug, instrument, warn};
 
-static MEC_KEYS_COMMANDS: Lazy<Vec<String>> = Lazy::new(|| {
+/// The method parameters is empty.
+static MEMCACHE_EMPTY_METHOD_MAPPING: Lazy<HashMap<&str, TagInfo<'static>>> = Lazy::new(|| {
     [
-        "set",
-        "setByKey",
-        "setMulti",
-        "setMultiByKey",
-        "add",
-        "addByKey",
-        "replace",
-        "replaceByKey",
-        "append",
-        "appendByKey",
-        "prepend",
-        "prependByKey",
-        "get",
-        "getByKey",
-        "getMulti",
-        "getMultiByKey",
-        "getAllKeys",
-        "delete",
-        "deleteByKey",
-        "deleteMulti",
-        "deleteMultiByKey",
-        "increment",
-        "incrementByKey",
-        "decrement",
-        "decrementByKey",
-        "getStats",
-        "isPersistent",
-        "isPristine",
-        "flush",
-        "flushBuffers",
-        "getDelayed",
-        "getDelayedByKey",
-        "fetch",
-        "fetchAll",
-        "addServer",
-        "addServers",
-        "getOption",
-        "setOption",
-        "setOptions",
-        "getResultCode",
-        "getServerList",
-        "resetServerList",
-        "getVersion",
-        "quit",
-        "setSaslAuthData",
-        "touch",
-        "touchByKey",
+        ("getallkeys", TagInfo::new(None, None)),
+        ("getstats", TagInfo::new(Some("stats"), None)),
+        ("flush", TagInfo::new(None, None)),
+        ("getversion", TagInfo::new(Some("version"), None)),
     ]
     .into_iter()
-    .map(str::to_ascii_lowercase)
     .collect()
 });
 
-static MEC_STR_KEYS_COMMANDS: Lazy<Vec<String>> = Lazy::new(|| {
+/// The method first parameter is key.
+static MEMCACHE_KEY_METHOD_MAPPING: Lazy<HashMap<&str, TagInfo<'static>>> = Lazy::new(|| {
     [
-        "set",
-        "setByKey",
-        "setMulti",
-        "setMultiByKey",
-        "add",
-        "addByKey",
-        "replace",
-        "replaceByKey",
-        "append",
-        "appendByKey",
-        "prepend",
-        "prependByKey",
-        "get",
-        "getByKey",
-        "getMulti",
-        "getMultiByKey",
-        "getAllKeys",
-        "delete",
-        "deleteByKey",
-        "deleteMulti",
-        "deleteMultiByKey",
-        "increment",
-        "incrementByKey",
-        "decrement",
-        "decrementByKey",
+        ("set", TagInfo::new(Some("set"), Some(CacheOp::Write))),
+        ("setmulti", TagInfo::new(Some("set"), Some(CacheOp::Write))),
+        ("add", TagInfo::new(Some("add"), Some(CacheOp::Write))),
+        (
+            "replace",
+            TagInfo::new(Some("replace"), Some(CacheOp::Write)),
+        ),
+        ("append", TagInfo::new(Some("append"), Some(CacheOp::Write))),
+        (
+            "prepend",
+            TagInfo::new(Some("prepend"), Some(CacheOp::Write)),
+        ),
+        ("get", TagInfo::new(Some("get"), Some(CacheOp::Read))),
+        ("getmulti", TagInfo::new(Some("get"), Some(CacheOp::Read))),
+        ("delete", TagInfo::new(Some("delete"), Some(CacheOp::Write))),
+        (
+            "deletemulti",
+            TagInfo::new(Some("deleteMulti"), Some(CacheOp::Write)),
+        ),
+        (
+            "increment",
+            TagInfo::new(Some("increment"), Some(CacheOp::Write)),
+        ),
+        (
+            "decrement",
+            TagInfo::new(Some("decrement"), Some(CacheOp::Write)),
+        ),
     ]
     .into_iter()
-    .map(str::to_ascii_lowercase)
     .collect()
 });
 
+/// The method first parameter is server key and second parameter is key.
+static MEMCACHE_SERVER_KEY_METHOD_MAPPING: Lazy<HashMap<&str, TagInfo<'static>>> =
+    Lazy::new(|| {
+        [
+            ("setByKey", TagInfo::new(Some("set"), Some(CacheOp::Write))),
+            (
+                "setMultiByKey",
+                TagInfo::new(Some("set"), Some(CacheOp::Write)),
+            ),
+            ("addByKey", TagInfo::new(Some("add"), Some(CacheOp::Write))),
+            (
+                "replaceByKey",
+                TagInfo::new(Some("replace"), Some(CacheOp::Write)),
+            ),
+            (
+                "appendByKey",
+                TagInfo::new(Some("append"), Some(CacheOp::Write)),
+            ),
+            (
+                "prependByKey",
+                TagInfo::new(Some("prepend"), Some(CacheOp::Write)),
+            ),
+            ("getByKey", TagInfo::new(Some("get"), Some(CacheOp::Read))),
+            (
+                "getMultiByKey",
+                TagInfo::new(Some("get"), Some(CacheOp::Read)),
+            ),
+            (
+                "deleteByKey",
+                TagInfo::new(Some("delete"), Some(CacheOp::Write)),
+            ),
+            (
+                "deleteMultiByKey",
+                TagInfo::new(Some("deleteMulti"), Some(CacheOp::Write)),
+            ),
+            (
+                "incrementByKey",
+                TagInfo::new(Some("increment"), Some(CacheOp::Write)),
+            ),
+            (
+                "decrementByKey",
+                TagInfo::new(Some("decrement"), Some(CacheOp::Write)),
+            ),
+        ]
+        .into_iter()
+        .collect()
+    });
+
+struct TagInfo<'a> {
+    cmd: Option<&'a str>,
+    op: Option<CacheOp>,
+}
+
+impl<'a> TagInfo<'a> {
+    fn new(cmd: Option<&'a str>, op: Option<CacheOp>) -> Self {
+        Self { cmd, op }
+    }
+}
+
 #[derive(Default, Clone)]
 pub struct MemcachedPlugin;
 
@@ -133,9 +158,19 @@
     )> {
         match (class_name, function_name) {
             (Some(class_name @ "Memcached"), f)
-                if MEC_KEYS_COMMANDS.contains(&f.to_ascii_lowercase()) =>
+                if MEMCACHE_EMPTY_METHOD_MAPPING.contains_key(&*f.to_ascii_lowercase()) =>
             {
-                Some(self.hook_memcached_methods(class_name, function_name))
+                Some(self.hook_memcached_empty_methods(class_name, function_name))
+            }
+            (Some(class_name @ "Memcached"), f)
+                if MEMCACHE_KEY_METHOD_MAPPING.contains_key(&*f.to_ascii_lowercase()) =>
+            {
+                Some(self.hook_memcached_key_methods(class_name, function_name))
+            }
+            (Some(class_name @ "Memcached"), f)
+                if MEMCACHE_SERVER_KEY_METHOD_MAPPING.contains_key(&*f.to_ascii_lowercase()) =>
+            {
+                Some(self.hook_memcached_server_key_methods(class_name, function_name))
             }
             _ => None,
         }
@@ -143,106 +178,214 @@
 }
 
 impl MemcachedPlugin {
-    fn hook_memcached_methods(
+    #[instrument(skip_all)]
+    fn hook_memcached_empty_methods(
+        &self, class_name: &str, function_name: &str,
+    ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+        let class_name = class_name.to_owned();
+        let function_name = function_name.to_owned();
+        (
+            Box::new(move |request_id, _| {
+                let tag_info = MEMCACHE_EMPTY_METHOD_MAPPING
+                    .get(&*function_name.to_ascii_lowercase())
+                    .unwrap();
+
+                let span =
+                    create_exit_span(request_id, &class_name, &function_name, "", tag_info, None)?;
+
+                Ok(Box::new(span))
+            }),
+            Box::new(after_hook),
+        )
+    }
+
+    #[instrument(skip_all)]
+    fn hook_memcached_key_methods(
         &self, class_name: &str, function_name: &str,
     ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
         let class_name = class_name.to_owned();
         let function_name = function_name.to_owned();
         (
             Box::new(move |request_id, execute_data| {
-                let peer = if MEC_STR_KEYS_COMMANDS.contains(&function_name.to_ascii_lowercase()) {
-                    let mut f = || {
-                        let key = {
-                            let key = execute_data.get_parameter(0);
-                            if !key.get_type_info().is_string() {
-                                // The `*Multi` methods will failed here.
-                                bail!("The argument key of {} isn't string", &function_name);
-                            }
-                            key.clone()
-                        };
-                        let this = get_this_mut(execute_data)?;
-                        let info = this.call(&"getServerByKey".to_ascii_lowercase(), [key])?;
-                        let info = info.as_z_arr().context("Server isn't array")?;
-                        let host = info
-                            .get("host")
-                            .context("Server host not exists")?
-                            .as_z_str()
-                            .context("Server host isn't string")?
-                            .to_str()?;
-                        let port = info
-                            .get("port")
-                            .context("Server port not exists")?
-                            .as_long()
-                            .context("Server port isn't long")?;
-                        Ok::<_, anyhow::Error>(format!("{}:{}", host, port))
-                    };
-                    match f() {
-                        Ok(peer) => peer,
-                        Err(err) => {
-                            warn!(?err, "Get peer failed");
-                            "".to_owned()
-                        }
+                let key = {
+                    let key = execute_data.get_parameter(0);
+                    if key.get_type_info().is_string() {
+                        Some(key.clone())
+                    } else {
+                        // The `*Multi` methods will failed here.
+                        warn!("The argument key of {} isn't string", &function_name);
+                        None
                     }
-                } else {
-                    "".to_owned()
                 };
 
+                let key_str = key
+                    .as_ref()
+                    .and_then(|key| key.as_z_str())
+                    .and_then(|key| key.to_str().ok())
+                    .map(ToOwned::to_owned);
+
+                let this = get_this_mut(execute_data)?;
+
+                let peer = key.map(|key| get_peer(this, key)).unwrap_or_default();
+
                 debug!(peer, "Get memcached peer");
 
-                let span = RequestContext::try_with_global_ctx(request_id, |ctx| {
-                    let mut span =
-                        ctx.create_exit_span(&format!("{}->{}", class_name, function_name), &peer);
-                    span.with_span_object_mut(|obj| {
-                        obj.set_span_layer(SpanLayer::Cache);
-                        obj.component_id = COMPONENT_PHP_MEMCACHED_ID;
-                        obj.add_tag("db.type", "memcached");
+                let tag_info = MEMCACHE_KEY_METHOD_MAPPING
+                    .get(&*function_name.to_ascii_lowercase())
+                    .unwrap();
 
-                        match get_command(execute_data, &function_name) {
-                            Ok(cmd) => {
-                                obj.add_tag("memcached.command", cmd);
-                            }
-                            Err(err) => {
-                                warn!(?err, "get command failed");
-                            }
-                        }
-                    });
-                    Ok(span)
-                })?;
+                let span = create_exit_span(
+                    request_id,
+                    &class_name,
+                    &function_name,
+                    &peer,
+                    tag_info,
+                    key_str.as_deref(),
+                )?;
 
-                Ok(Box::new(span) as _)
+                Ok(Box::new(span))
             }),
-            Box::new(|_, span, _, return_value| {
-                let mut span = span.downcast::<Span>().unwrap();
-                if let Some(b) = return_value.as_bool() {
-                    if !b {
-                        span.with_span_object_mut(|span| {
-                            span.is_error = true;
-                        });
+            Box::new(after_hook),
+        )
+    }
+
+    #[instrument(skip_all)]
+    fn hook_memcached_server_key_methods(
+        &self, class_name: &str, function_name: &str,
+    ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+        let class_name = class_name.to_owned();
+        let function_name = function_name.to_owned();
+        (
+            Box::new(move |request_id, execute_data| {
+                let server_key = {
+                    let server_key = execute_data.get_parameter(0);
+                    if server_key.get_type_info().is_string() {
+                        Some(server_key.clone())
+                    } else {
+                        // The `*Multi` methods will failed here.
+                        warn!(function_name, "The argument server_key isn't string");
+                        None
                     }
-                }
-                Ok(())
+                };
+
+                let key = execute_data
+                    .get_parameter(1)
+                    .as_z_str()
+                    .and_then(|key| key.to_str().ok())
+                    .map(ToOwned::to_owned);
+
+                let this = get_this_mut(execute_data)?;
+
+                let peer = server_key
+                    .map(|server_key| get_peer(this, server_key))
+                    .unwrap_or_default();
+
+                debug!(peer, "Get memcached peer");
+
+                let tag_info = MEMCACHE_SERVER_KEY_METHOD_MAPPING
+                    .get(&*function_name.to_ascii_lowercase())
+                    .unwrap();
+
+                let span = create_exit_span(
+                    request_id,
+                    &class_name,
+                    &function_name,
+                    &peer,
+                    tag_info,
+                    key.as_deref(),
+                )?;
+
+                Ok(Box::new(span))
             }),
+            Box::new(after_hook),
         )
     }
 }
 
-fn get_command(execute_data: &mut ExecuteData, function_name: &str) -> anyhow::Result<String> {
-    let num_args = execute_data.num_args();
-    let mut items = Vec::with_capacity(num_args + 1);
-    items.push(function_name.to_owned());
+#[instrument(skip_all)]
+fn after_hook(
+    _: Option<i64>, span: Box<dyn Any>, execute_data: &mut ExecuteData, return_value: &mut ZVal,
+) -> anyhow::Result<()> {
+    let mut span = span.downcast::<Span>().expect("Downcast to Span failed");
+    if let Some(b) = return_value.as_bool() {
+        if !b {
+            span.with_span_object_mut(|span| {
+                span.is_error = true;
+            });
 
-    for i in 0..num_args {
-        let parameter = execute_data.get_parameter(i);
-        let s = if parameter.get_type_info().is_array() {
-            let result = call("json_encode", [parameter.clone()])?;
-            result.expect_z_str()?.to_str()?.to_string()
-        } else {
-            let mut parameter = parameter.clone();
-            parameter.convert_to_string();
-            parameter.expect_z_str()?.to_str()?.to_string()
-        };
-        items.push(s)
+            let this = get_this_mut(execute_data)?;
+            let code = this.call(&"getResultCode".to_ascii_lowercase(), [])?;
+            let code = code.as_long().context("ResultCode isn't int")?;
+            debug!(code, "get memcached result code");
+
+            if code != 0 {
+                let message = this.call(&"getResultMessage".to_ascii_lowercase(), [])?;
+                let message = message
+                    .as_z_str()
+                    .context("ResultMessage isn't string")?
+                    .to_str()?;
+                debug!(message, "get memcached result message");
+
+                span.add_log([
+                    ("ResultCode", code.to_string()),
+                    ("ResultMessage", message.to_owned()),
+                ]);
+            }
+        }
     }
+    Ok(())
+}
 
-    Ok(items.join(" "))
+fn create_exit_span<'a>(
+    request_id: Option<i64>, class_name: &str, function_name: &str, remote_peer: &str,
+    tag_info: &TagInfo<'a>, key: Option<&str>,
+) -> anyhow::Result<Span> {
+    RequestContext::try_with_global_ctx(request_id, |ctx| {
+        let mut span =
+            ctx.create_exit_span(&format!("{}->{}", class_name, function_name), remote_peer);
+
+        span.with_span_object_mut(|obj| {
+            obj.set_span_layer(SpanLayer::Cache);
+            obj.component_id = COMPONENT_PHP_MEMCACHED_ID;
+            obj.add_tag(TAG_CACHE_TYPE, "memcache");
+
+            if let Some(cmd) = &tag_info.cmd {
+                obj.add_tag(TAG_CACHE_CMD, cmd);
+            }
+            if let Some(op) = &tag_info.op {
+                obj.add_tag(TAG_CACHE_OP, op.to_string());
+            };
+            if let Some(key) = key {
+                obj.add_tag(TAG_CACHE_KEY, key)
+            }
+        });
+
+        Ok(span)
+    })
+}
+
+fn get_peer(this: &mut ZObj, key: ZVal) -> String {
+    let f = || {
+        let info = {
+            let _e = ExceptionFrame::new();
+            this.call(&"getServerByKey".to_ascii_lowercase(), [key])?
+        };
+        let info = info.as_z_arr().context("Server isn't array")?;
+        let host = info
+            .get("host")
+            .context("Server host not exists")?
+            .as_z_str()
+            .context("Server host isn't string")?
+            .to_str()?;
+        let port = info
+            .get("port")
+            .context("Server port not exists")?
+            .as_long()
+            .context("Server port isn't long")?;
+        Ok::<_, anyhow::Error>(format!("{}:{}", host, port))
+    };
+    f().unwrap_or_else(|err| {
+        warn!(?err, "Get peer failed");
+        "".to_owned()
+    })
 }
diff --git a/src/plugin/plugin_pdo.rs b/src/plugin/plugin_pdo.rs
index 2a349bc..9febec4 100644
--- a/src/plugin/plugin_pdo.rs
+++ b/src/plugin/plugin_pdo.rs
@@ -19,6 +19,7 @@
     context::RequestContext,
     exception_frame::ExceptionFrame,
     execute::{get_this_mut, validate_num_args, AfterExecuteHook, BeforeExecuteHook, Noop},
+    tag::{TAG_DB_STATEMENT, TAG_DB_TYPE},
 };
 use anyhow::Context;
 use dashmap::DashMap;
@@ -120,7 +121,7 @@
 
                 if execute_data.num_args() >= 1 {
                     if let Some(statement) = execute_data.get_parameter(0).as_z_str() {
-                        span.add_tag("db.statement", statement.to_str()?);
+                        span.add_tag(TAG_DB_STATEMENT, statement.to_str()?);
                     }
                 }
 
@@ -146,7 +147,7 @@
                 })?;
 
                 if let Some(query) = this.get_property("queryString").as_z_str() {
-                    span.add_tag("db.statement", query.to_str()?);
+                    span.add_tag(TAG_DB_STATEMENT, query.to_str()?);
                 } else {
                     warn!("PDOStatement queryString is empty");
                 }
@@ -256,7 +257,7 @@
         span.with_span_object_mut(|obj| {
             obj.set_span_layer(SpanLayer::Database);
             obj.component_id = COMPONENT_PHP_PDO_ID;
-            obj.add_tag("db.type", &dsn.db_type);
+            obj.add_tag(TAG_DB_TYPE, &dsn.db_type);
             obj.add_tag("db.data_source", &dsn.data_source);
         });
         Ok(span)
diff --git a/src/plugin/plugin_predis.rs b/src/plugin/plugin_predis.rs
index 772dd3b..918a954 100644
--- a/src/plugin/plugin_predis.rs
+++ b/src/plugin/plugin_predis.rs
@@ -20,10 +20,116 @@
     component::COMPONENT_PHP_PREDIS_ID,
     context::RequestContext,
     execute::{get_this_mut, validate_num_args, AfterExecuteHook, BeforeExecuteHook},
+    tag::{TAG_CACHE_CMD, TAG_CACHE_KEY, TAG_CACHE_OP, TAG_CACHE_TYPE},
 };
 use anyhow::Context;
-use phper::arrays::ZArr;
+use once_cell::sync::Lazy;
 use skywalking::{skywalking_proto::v3::SpanLayer, trace::span::Span};
+use std::collections::HashSet;
+use tracing::debug;
+
+pub static REDIS_READ_COMMANDS: Lazy<HashSet<&str>> = Lazy::new(|| {
+    [
+        "BLPOP",
+        "BRPOP",
+        "GET",
+        "GETBIT",
+        "GETRANGE",
+        "HEXISTS",
+        "HGET",
+        "HGETALL",
+        "HKEYS",
+        "HLEN",
+        "HMGET",
+        "HSCAN",
+        "HSTRLEN",
+        "HVALS",
+        "KEYS",
+        "LGET",
+        "LGETRANGE",
+        "LLEN",
+        "LRANGE",
+        "LSIZE",
+        "MGET",
+        "SCONTAINS",
+        "SGETMEMBERS",
+        "SISMEMBER",
+        "SMEMBERS",
+        "SSCAN",
+        "SSIZE",
+        "STRLEN",
+        "ZCOUNT",
+        "ZRANGE",
+        "ZRANGEBYLEX",
+        "ZRANGEBYSCORE",
+        "ZSCAN",
+        "ZSIZE",
+    ]
+    .into_iter()
+    .collect()
+});
+
+pub static REDIS_WRITE_COMMANDS: Lazy<HashSet<&str>> = Lazy::new(|| {
+    [
+        "APPEND",
+        "BRPOPLPUSH",
+        "DECR",
+        "DECRBY",
+        "DEL",
+        "DELETE",
+        "HDEL",
+        "HINCRBY",
+        "HINCRBYFLOAT",
+        "HMSET",
+        "HSET",
+        "HSETNX",
+        "INCR",
+        "INCRBY",
+        "INCRBYFLOAT",
+        "LINSERT",
+        "LPUSH",
+        "LPUSHX",
+        "LREM",
+        "LREMOVE",
+        "LSET",
+        "LTRIM",
+        "LISTTRIM",
+        "MSET",
+        "MSETNX",
+        "PSETEX",
+        "RPOPLPUSH",
+        "RPUSH",
+        "RPUSHX",
+        "RANDOMKEY",
+        "SADD",
+        "SINTER",
+        "SINTERSTORE",
+        "SMOVE",
+        "SRANDMEMBER",
+        "SREM",
+        "SREMOVE",
+        "SET",
+        "SETBIT",
+        "SETEX",
+        "SETNX",
+        "SETRANGE",
+        "SETTIMEOUT",
+        "SORT",
+        "UNLINK",
+        "ZADD",
+        "ZDELETE",
+        "ZDELETERANGEBYRANK",
+        "ZDELETERANGEBYSCORE",
+        "ZINCRBY",
+        "ZREM",
+        "ZREMRANGEBYRANK",
+        "ZREMRANGEBYSCORE",
+        "ZREMOVE",
+        "ZREMOVERANGEBYSCORE",
+    ]
+    .into_iter()
+    .collect()
+});
 
 #[derive(Default, Clone)]
 pub struct PredisPlugin;
@@ -44,9 +150,10 @@
         Box<crate::execute::AfterExecuteHook>,
     )> {
         match (class_name, function_name) {
-            (Some(class_name @ "Predis\\Connection\\AbstractConnection"), "executeCommand") => {
-                Some(self.hook_predis_execute_command(class_name))
-            }
+            (
+                Some(class_name @ "Predis\\Connection\\AbstractConnection"),
+                function_name @ "executeCommand",
+            ) => Some(self.hook_predis_execute_command(class_name, function_name)),
             _ => None,
         }
     }
@@ -54,9 +161,10 @@
 
 impl PredisPlugin {
     fn hook_predis_execute_command(
-        &self, class_name: &str,
+        &self, class_name: &str, function_name: &str,
     ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
         let class_name = class_name.to_owned();
+        let function_name = function_name.to_owned();
         (
             Box::new(move |request_id, execute_data| {
                 validate_num_args(execute_data, 1)?;
@@ -77,25 +185,56 @@
                     .expect_long()?;
                 let peer = format!("{}:{}", host, port);
 
+                let handle = this.handle();
                 let command = execute_data.get_parameter(0).expect_mut_z_obj()?;
+                let command_class_name = command
+                    .get_class()
+                    .get_name()
+                    .to_str()
+                    .map(ToOwned::to_owned)
+                    .unwrap_or_default();
 
                 let id = command.call("getid", []).context("call getId failed")?;
-                let id = id.expect_z_str()?.to_str()?;
+                let cmd = id.expect_z_str()?.to_str()?.to_ascii_uppercase();
 
                 let mut arguments = command
                     .call("getarguments", [])
                     .context("call getArguments failed")?;
                 let arguments = arguments.expect_mut_z_arr()?;
 
+                let op = if REDIS_READ_COMMANDS.contains(&*cmd) {
+                    Some("read")
+                } else if REDIS_WRITE_COMMANDS.contains(&*cmd) {
+                    Some("write")
+                } else {
+                    None
+                };
+
+                let key = op
+                    .and_then(|_| arguments.get(0))
+                    .and_then(|arg| arg.as_z_str())
+                    .and_then(|s| s.to_str().ok());
+
+                debug!(handle, cmd, key, op, "call redis command");
+
                 let mut span = RequestContext::try_with_global_ctx(request_id, |ctx| {
-                    Ok(ctx.create_exit_span(&format!("{}->{}", class_name, id), &peer))
+                    Ok(ctx.create_exit_span(
+                        &format!("{}->{}({})", class_name, function_name, command_class_name),
+                        &peer,
+                    ))
                 })?;
 
                 span.with_span_object_mut(|span| {
                     span.set_span_layer(SpanLayer::Cache);
                     span.component_id = COMPONENT_PHP_PREDIS_ID;
-                    span.add_tag("db.type", "redis");
-                    span.add_tag("redis.command", generate_command(id, arguments));
+                    span.add_tag(TAG_CACHE_TYPE, "redis");
+                    span.add_tag(TAG_CACHE_CMD, cmd);
+                    if let Some(op) = op {
+                        span.add_tag(TAG_CACHE_OP, op);
+                    };
+                    if let Some(key) = key {
+                        span.add_tag(TAG_CACHE_KEY, key)
+                    }
                 });
 
                 Ok(Box::new(span))
@@ -113,18 +252,3 @@
         )
     }
 }
-
-fn generate_command(id: &str, arguments: &mut ZArr) -> String {
-    let mut ss = Vec::with_capacity(arguments.len() + 1);
-    ss.push(id);
-
-    for (_, argument) in arguments.iter() {
-        if let Some(value) = argument.as_z_str().and_then(|s| s.to_str().ok()) {
-            ss.push(value);
-        } else if argument.as_z_arr().is_some() {
-            break;
-        }
-    }
-
-    ss.join(" ")
-}
diff --git a/src/plugin/plugin_redis.rs b/src/plugin/plugin_redis.rs
index e376d07..1a29307 100644
--- a/src/plugin/plugin_redis.rs
+++ b/src/plugin/plugin_redis.rs
@@ -13,8 +13,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use std::{any::Any, collections::HashSet};
-
 use super::Plugin;
 use crate::{
     component::COMPONENT_PHP_REDIS_ID,
@@ -32,123 +30,128 @@
     values::{ExecuteData, ZVal},
 };
 use skywalking::{skywalking_proto::v3::SpanLayer, trace::span::Span};
+use std::{any::Any, collections::HashMap};
 use tracing::{debug, warn};
 
 static PEER_MAP: Lazy<DashMap<u32, Peer>> = Lazy::new(Default::default);
 
 static FREE_MAP: Lazy<DashMap<u32, sys::zend_object_free_obj_t>> = Lazy::new(Default::default);
 
-static REDIS_READ_COMMANDS: Lazy<HashSet<String>> = Lazy::new(|| {
+static REDIS_READ_MAPPING: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
     [
-        "blPop",
-        "brPop",
-        "get",
-        "getBit",
-        "getKeys",
-        "getMultiple",
-        "getRange",
-        "hExists",
-        "hGet",
-        "hGetAll",
-        "hKeys",
-        "hLen",
-        "hMGet",
-        "hScan",
-        "hStrLen",
-        "hVals",
-        "keys",
-        "lGet",
-        "lGetRange",
-        "lLen",
-        "lRange",
-        "lSize",
-        "mGet",
-        "sContains",
-        "sGetMembers",
-        "sIsMember",
-        "sMembers",
-        "sScan",
-        "sSize",
-        "strLen",
-        "zCount",
-        "zRange",
-        "zRangeByLex",
-        "zRangeByScore",
-        "zScan",
-        "zSize",
+        ("blpop", "BLPOP"),
+        ("brpop", "BRPOP"),
+        ("get", "GET"),
+        ("getbit", "GETBIT"),
+        ("getkeys", "KEYS"),
+        ("getmultiple", "MGET"),
+        ("getrange", "GETRANGE"),
+        ("hexists", "HEXISTS"),
+        ("hget", "HGET"),
+        ("hgetall", "HGETALL"),
+        ("hkeys", "HKEYS"),
+        ("hlen", "HLEN"),
+        ("hmget", "HMGET"),
+        ("hscan", "HSCAN"),
+        ("hstrlen", "HSTRLEN"),
+        ("hvals", "HVALS"),
+        ("keys", "KEYS"),
+        ("lget", "LGET"),
+        ("lgetrange", "LGETRANGE"),
+        ("llen", "LLEN"),
+        ("lrange", "LRANGE"),
+        ("lsize", "LSIZE"),
+        ("mget", "MGET"),
+        ("mget", "MGET"),
+        ("scontains", "SCONTAINS"),
+        ("sgetmembers", "SGETMEMBERS"),
+        ("sismember", "SISMEMBER"),
+        ("smembers", "SMEMBERS"),
+        ("sscan", "SSCAN"),
+        ("ssize", "SSIZE"),
+        ("strlen", "STRLEN"),
+        ("substr", "GETRANGE"),
+        ("zcount", "ZCOUNT"),
+        ("zrange", "ZRANGE"),
+        ("zrangebylex", "ZRANGEBYLEX"),
+        ("zrangebyscore", "ZRANGEBYSCORE"),
+        ("zscan", "ZSCAN"),
+        ("zsize", "ZSIZE"),
     ]
     .into_iter()
-    .map(str::to_ascii_lowercase)
     .collect()
 });
 
-static REDIS_WRITE_COMMANDS: Lazy<HashSet<String>> = Lazy::new(|| {
+static REDIS_WRITE_MAPPING: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
     [
-        "append",
-        "bRPopLPush",
-        "decr",
-        "decrBy",
-        "del",
-        "delete",
-        "hDel",
-        "hIncrBy",
-        "hIncrByFloat",
-        "hMSet",
-        "hSet",
-        "hSetNx",
-        "incr",
-        "incrBy",
-        "incrByFloat",
-        "lInsert",
-        "lPush",
-        "lPushx",
-        "lRem",
-        "lRemove",
-        "lSet",
-        "lTrim",
-        "listTrim",
-        "mSet",
-        "mSetNX",
-        "pSetEx",
-        "rPopLPush",
-        "rPush",
-        "rPushX",
-        "randomKey",
-        "sAdd",
-        "sInter",
-        "sInterStore",
-        "sMove",
-        "sRandMember",
-        "sRem",
-        "sRemove",
-        "set",
-        "setBit",
-        "setEx",
-        "setNx",
-        "setRange",
-        "setTimeout",
-        "sort",
-        "unlink",
-        "zAdd",
-        "zDelete",
-        "zDeleteRangeByRank",
-        "zDeleteRangeByScore",
-        "zIncrBy",
-        "zRem",
-        "zRemRangeByRank",
-        "zRemRangeByScore",
-        "zRemove",
-        "zRemoveRangeByScore",
+        ("append", "APPEND"),
+        ("brpoplpush", "BRPOPLPUSH"),
+        ("decr", "DECR"),
+        ("decrby", "DECRBY"),
+        ("del", "DEL"),
+        ("delete", "DEL"),
+        ("hdel", "HDEL"),
+        ("hincrby", "HINCRBY"),
+        ("hincrbyfloat", "HINCRBYFLOAT"),
+        ("hmset", "HMSET"),
+        ("hset", "HSET"),
+        ("hsetnx", "HSETNX"),
+        ("incr", "INCR"),
+        ("incrby", "INCRBY"),
+        ("incrbyfloat", "INCRBYFLOAT"),
+        ("linsert", "LINSERT"),
+        ("lpush", "LPUSH"),
+        ("lpushx", "LPUSHX"),
+        ("lrem", "LREM"),
+        ("lremove", "LREMOVE"),
+        ("lset", "LSET"),
+        ("ltrim", "LTRIM"),
+        ("listtrim", "LISTTRIM"),
+        ("mset", "MSET"),
+        ("msetnx", "MSETNX"),
+        ("psetex", "PSETEX"),
+        ("rpoplpush", "RPOPLPUSH"),
+        ("rpush", "RPUSH"),
+        ("rpushx", "RPUSHX"),
+        ("randomkey", "RANDOMKEY"),
+        ("sadd", "SADD"),
+        ("sinter", "SINTER"),
+        ("sinterstore", "SINTERSTORE"),
+        ("smove", "SMOVE"),
+        ("srandmember", "SRANDMEMBER"),
+        ("srem", "SREM"),
+        ("sremove", "SREMOVE"),
+        ("set", "SET"),
+        ("setbit", "SETBIT"),
+        ("setex", "SETEX"),
+        ("setnx", "SETNX"),
+        ("setrange", "SETRANGE"),
+        ("settimeout", "SETTIMEOUT"),
+        ("sort", "SORT"),
+        ("unlink", "UNLINK"),
+        ("zadd", "ZADD"),
+        ("zdelete", "ZDELETE"),
+        ("zdeleterangebyrank", "ZDELETERANGEBYRANK"),
+        ("zdeleterangebyscore", "ZDELETERANGEBYSCORE"),
+        ("zincrby", "ZINCRBY"),
+        ("zrem", "ZREM"),
+        ("zremrangebyrank", "ZREMRANGEBYRANK"),
+        ("zremrangebyscore", "ZREMRANGEBYSCORE"),
+        ("zremove", "ZREMOVE"),
+        ("zremoverangebyscore", "ZREMOVERANGEBYSCORE"),
     ]
     .into_iter()
-    .map(str::to_ascii_lowercase)
     .collect()
 });
 
-static REDIS_ALL_COMMANDS: Lazy<HashSet<String>> = Lazy::new(|| {
-    let mut commands = HashSet::new();
-    commands.extend(REDIS_READ_COMMANDS.iter().map(Clone::clone));
-    commands.extend(REDIS_WRITE_COMMANDS.iter().map(Clone::clone));
+static REDIS_OTHER_MAPPING: Lazy<HashMap<&str, &str>> =
+    Lazy::new(|| [("auth", "AUTH")].into_iter().collect());
+
+static REDIS_ALL_MAPPING: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
+    let mut commands = HashMap::with_capacity(REDIS_READ_MAPPING.len() + REDIS_WRITE_MAPPING.len());
+    commands.extend(REDIS_READ_MAPPING.iter());
+    commands.extend(REDIS_WRITE_MAPPING.iter());
+    commands.extend(REDIS_OTHER_MAPPING.iter());
     commands
 });
 
@@ -177,7 +180,7 @@
                 Some(self.hook_redis_connect(class_name, function_name))
             }
             (Some(class_name @ "Redis"), f)
-                if REDIS_ALL_COMMANDS.contains(&f.to_ascii_lowercase()) =>
+                if REDIS_ALL_MAPPING.contains_key(&*f.to_ascii_lowercase()) =>
             {
                 Some(self.hook_redis_methods(class_name, function_name))
             }
@@ -283,17 +286,21 @@
                     .map(|r| r.value().addr.clone())
                     .unwrap_or_default();
 
-                let key = execute_data
-                    .get_parameter(0)
-                    .as_z_str()
-                    .and_then(|s| s.to_str().ok());
-                let op = if REDIS_READ_COMMANDS.contains(&function_name.to_ascii_lowercase()) {
-                    "read"
+                let function_name_key = &*function_name.to_ascii_lowercase();
+
+                let op = if REDIS_READ_MAPPING.contains_key(function_name_key) {
+                    Some("read")
+                } else if REDIS_WRITE_MAPPING.contains_key(function_name_key) {
+                    Some("write")
                 } else {
-                    "write"
+                    None
                 };
 
-                debug!(handle, function_name, key, op, "call redis command");
+                let key = op
+                    .and_then(|_| execute_data.get_parameter(0).as_z_str())
+                    .and_then(|s| s.to_str().ok());
+
+                debug!(handle, cmd = function_name, key, op, "call redis command");
 
                 let mut span = RequestContext::try_with_global_ctx(request_id, |ctx| {
                     Ok(ctx.create_exit_span(&format!("{}->{}", class_name, function_name), &peer))
@@ -303,8 +310,13 @@
                     span.set_span_layer(SpanLayer::Cache);
                     span.component_id = COMPONENT_PHP_REDIS_ID;
                     span.add_tag(TAG_CACHE_TYPE, "redis");
-                    span.add_tag(TAG_CACHE_CMD, function_name);
-                    span.add_tag(TAG_CACHE_OP, op);
+                    span.add_tag(
+                        TAG_CACHE_CMD,
+                        REDIS_ALL_MAPPING.get(function_name_key).unwrap(),
+                    );
+                    if let Some(op) = op {
+                        span.add_tag(TAG_CACHE_OP, op);
+                    }
                     if let Some(key) = key {
                         span.add_tag(TAG_CACHE_KEY, key)
                     }
diff --git a/src/tag.rs b/src/tag.rs
index 69ca800..7b0abd1 100644
--- a/src/tag.rs
+++ b/src/tag.rs
@@ -18,8 +18,31 @@
 //! Virtual Cache
 //!
 //! https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-cache/
+//!
+//! Virtual Database
+//!
+//! https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-database/
+
+use std::fmt::Display;
 
 pub const TAG_CACHE_TYPE: &str = "cache.type";
 pub const TAG_CACHE_OP: &str = "cache.op";
 pub const TAG_CACHE_CMD: &str = "cache.cmd";
 pub const TAG_CACHE_KEY: &str = "cache.key";
+
+pub enum CacheOp {
+    Read,
+    Write,
+}
+
+impl Display for CacheOp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Read => write!(f, "read"),
+            Self::Write => write!(f, "write"),
+        }
+    }
+}
+
+pub const TAG_DB_STATEMENT: &str = "db.statement";
+pub const TAG_DB_TYPE: &str = "db.type";
diff --git a/tests/data/expected_context.yaml b/tests/data/expected_context.yaml
index f366211..e4db16c 100644
--- a/tests/data/expected_context.yaml
+++ b/tests/data/expected_context.yaml
@@ -454,7 +454,7 @@
               - { key: http.status_code, value: "200" }
       - segmentId: "not null"
         spans:
-          - operationName: "Predis\\Connection\\AbstractConnection->AUTH"
+          - operationName: "Predis\\Connection\\AbstractConnection->executeCommand(Predis\\Command\\Redis\\AUTH)"
             parentSpanId: 0
             spanId: 1
             spanLayer: Cache
@@ -466,9 +466,9 @@
             peer: 127.0.0.1:6379
             skipAnalysis: false
             tags:
-              - { key: db.type, value: redis }
-              - { key: redis.command, value: AUTH password }
-          - operationName: "Predis\\Connection\\AbstractConnection->SET"
+              - { key: cache.type, value: redis }
+              - { key: cache.cmd, value: AUTH }
+          - operationName: "Predis\\Connection\\AbstractConnection->executeCommand(Predis\\Command\\Redis\\SET)"
             parentSpanId: 0
             spanId: 2
             spanLayer: Cache
@@ -480,9 +480,11 @@
             peer: 127.0.0.1:6379
             skipAnalysis: false
             tags:
-              - { key: db.type, value: redis }
-              - { key: redis.command, value: SET foo bar }
-          - operationName: "Predis\\Connection\\AbstractConnection->GET"
+              - { key: cache.type, value: redis }
+              - { key: cache.cmd, value: SET }
+              - { key: cache.op, value: write }
+              - { key: cache.key, value: foo }
+          - operationName: "Predis\\Connection\\AbstractConnection->executeCommand(Predis\\Command\\Redis\\GET)"
             parentSpanId: 0
             spanId: 3
             spanLayer: Cache
@@ -494,9 +496,11 @@
             peer: 127.0.0.1:6379
             skipAnalysis: false
             tags:
-              - { key: db.type, value: redis }
-              - { key: redis.command, value: GET foo }
-          - operationName: "Predis\\Connection\\AbstractConnection->GET"
+              - { key: cache.type, value: redis }
+              - { key: cache.cmd, value: GET }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: foo }
+          - operationName: "Predis\\Connection\\AbstractConnection->executeCommand(Predis\\Command\\Redis\\GET)"
             parentSpanId: 0
             spanId: 4
             spanLayer: Cache
@@ -508,8 +512,10 @@
             peer: 127.0.0.1:6379
             skipAnalysis: false
             tags:
-              - { key: db.type, value: redis }
-              - { key: redis.command, value: GET not-exists }
+              - { key: cache.type, value: redis }
+              - { key: cache.cmd, value: GET }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: not-exists }
           - operationName: GET:/predis.php
             parentSpanId: -1
             spanId: 0
@@ -578,7 +584,7 @@
               - {key: http.status_code, value: '200'}
       - segmentId: 'not null'
         spans:
-          - operationName: Memcached->addServer
+          - operationName: Memcached->set
             parentSpanId: 0
             spanId: 1
             spanLayer: Cache
@@ -587,11 +593,13 @@
             componentId: 20
             isError: false
             spanType: Exit
-            peer: ""
+            peer: 127.0.0.1:11211
             skipAnalysis: false
             tags:
-              - { key: db.type, value: memcached }
-              - { key: memcached.command, value: addServer 127.0.0.1 11211 }
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: set }
+              - { key: cache.op, value: write }
+              - { key: cache.key, value: foo }
           - operationName: Memcached->set
             parentSpanId: 0
             spanId: 2
@@ -604,9 +612,11 @@
             peer: 127.0.0.1:11211
             skipAnalysis: false
             tags:
-              - { key: db.type, value: memcached }
-              - { key: memcached.command, value: set foo Hello! }
-          - operationName: Memcached->set
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: set }
+              - { key: cache.op, value: write }
+              - { key: cache.key, value: bar }
+          - operationName: Memcached->get
             parentSpanId: 0
             spanId: 3
             spanLayer: Cache
@@ -618,8 +628,10 @@
             peer: 127.0.0.1:11211
             skipAnalysis: false
             tags:
-              - { key: db.type, value: memcached }
-              - { key: memcached.command, value: set bar Memcached... }
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: get }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: foo }
           - operationName: Memcached->get
             parentSpanId: 0
             spanId: 4
@@ -632,25 +644,13 @@
             peer: 127.0.0.1:11211
             skipAnalysis: false
             tags:
-              - { key: db.type, value: memcached }
-              - { key: memcached.command, value: get foo }
-          - operationName: Memcached->get
-            parentSpanId: 0
-            spanId: 5
-            spanLayer: Cache
-            startTime: gt 0
-            endTime: gt 0
-            componentId: 20
-            isError: false
-            spanType: Exit
-            peer: 127.0.0.1:11211
-            skipAnalysis: false
-            tags:
-              - { key: db.type, value: memcached }
-              - { key: memcached.command, value: get bar }
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: get }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: bar }
           - operationName: Memcached->setMulti
             parentSpanId: 0
-            spanId: 6
+            spanId: 5
             spanLayer: Cache
             startTime: gt 0
             endTime: gt 0
@@ -660,12 +660,29 @@
             peer: ""
             skipAnalysis: false
             tags:
-              - { key: db.type, value: memcached }
-              - {
-                  key: memcached.command,
-                  value:
-                    'setMulti {"key1":"value1","key2":"value2","key3":"value3"}',
-                }
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: set }
+              - { key: cache.op, value: write }
+          - operationName: Memcached->get
+            parentSpanId: 0
+            spanId: 6
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: true
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: memcache }
+              - { key: cache.cmd, value: get }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: not-exists }
+            logs:
+              - logEvent:
+                  - { key: ResultCode, value: "16" }
+                  - { key: ResultMessage, value: NOT FOUND }
           - operationName: GET:/memcached.php
             parentSpanId: -1
             spanId: 0
@@ -697,7 +714,7 @@
             tags:
               - key: cache.type
                 value: redis
-          - operationName: Redis->mset
+          - operationName: Redis->auth
             parentSpanId: 0
             spanId: 2
             spanLayer: Cache
@@ -709,13 +726,9 @@
             peer: 127.0.0.1:6379
             skipAnalysis: false
             tags:
-              - key: cache.type
-                value: redis
-              - key: cache.cmd
-                value: mset
-              - key: cache.op
-                value: write
-          - operationName: Redis->get
+              - { key: cache.type, value: redis }
+              - { key: cache.cmd, value: AUTH }
+          - operationName: Redis->mset
             parentSpanId: 0
             spanId: 3
             spanLayer: Cache
@@ -727,14 +740,9 @@
             peer: 127.0.0.1:6379
             skipAnalysis: false
             tags:
-              - key: cache.type
-                value: redis
-              - key: cache.cmd
-                value: get
-              - key: cache.op
-                value: read
-              - key: cache.key
-                value: key0
+              - { key: cache.type, value: redis }
+              - { key: cache.cmd, value: MSET }
+              - { key: cache.op, value: write }
           - operationName: Redis->get
             parentSpanId: 0
             spanId: 4
@@ -747,14 +755,26 @@
             peer: 127.0.0.1:6379
             skipAnalysis: false
             tags:
-              - key: cache.type
-                value: redis
-              - key: cache.cmd
-                value: get
-              - key: cache.op
-                value: read
-              - key: cache.key
-                value: key1
+              - { key: cache.type, value: redis }
+              - { key: cache.cmd, value: GET }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: key0 }
+          - operationName: Redis->get
+            parentSpanId: 0
+            spanId: 5
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:6379
+            skipAnalysis: false
+            tags:
+              - { key: cache.type, value: redis }
+              - { key: cache.cmd, value: GET }
+              - { key: cache.op, value: read }
+              - { key: cache.key, value: key1 }
           - operationName: GET:/redis.succ.php
             parentSpanId: -1
             spanId: 0
@@ -784,8 +804,7 @@
             peer: "127.0.0.1:6379"
             skipAnalysis: false
             tags:
-              - key: cache.type
-                value: redis
+              - { key: cache.type, value: redis }
           - operationName: Redis->set
             parentSpanId: 0
             spanId: 2
@@ -798,14 +817,10 @@
             peer: 127.0.0.1:6379
             skipAnalysis: false
             tags:
-              - key: cache.type
-                value: redis
-              - key: cache.cmd
-                value: set
-              - key: cache.op
-                value: write
-              - key: cache.key
-                value: foo
+              - { key: cache.type, value: redis }
+              - { key: cache.cmd, value: SET }
+              - { key: cache.op, value: write }
+              - { key: cache.key, value: foo }
             logs:
               - logEvent:
                   - { key: Exception Class, value: RedisException }
diff --git a/tests/php/fpm/memcached.php b/tests/php/fpm/memcached.php
index dd59904..da5c849 100644
--- a/tests/php/fpm/memcached.php
+++ b/tests/php/fpm/memcached.php
@@ -30,12 +30,13 @@
     Assert::same($mc->get("foo"), 'Hello!');
     Assert::same($mc->get("bar"), "Memcached...");
 
-    $items = array(
+    $mc->setMulti(array(
         'key1' => 'value1',
         'key2' => 'value2',
         'key3' => 'value3'
-    );
-    $mc->setMulti($items);
+    ));
+
+    Assert::false($mc->get("not-exists"));
 }
 
 echo "ok";