using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Text;
namespace CYQ.Data.Cache
{
///
/// Memcached client main class.
/// Use the static methods Setup and GetInstance to setup and get an instance of the client for use.
///
internal class RedisClient
{
#region Static fields and methods.
private static Dictionary instances = new Dictionary();
private static LogAdapter logger = LogAdapter.GetLogger(typeof(RedisClient));
///
/// Static method for creating an instance. This method will throw an exception if the name already exists.
///
/// The name of the instance.
/// A list of memcached servers in standard notation: host:port.
/// If port is omitted, the default value of 11211 is used.
/// Both IP addresses and host names are accepted, for example:
/// "localhost", "127.0.0.1", "cache01.example.com:12345", "127.0.0.1:12345", etc.
public static RedisClient Setup(string name, string[] servers)
{
if (instances.ContainsKey(name))
{
Error.Throw("Trying to configure RedisClient instance \"" + name + "\" twice.");
}
RedisClient client = new RedisClient(name, servers);
instances[name] = client;
return client;
}
///
/// Static method which checks if a given named RedisClient instance exists.
///
/// The name of the instance.
///
public static bool Exists(string name)
{
return instances.ContainsKey(name);
}
///
/// Static method for getting the default instance named "default".
///
private static RedisClient defaultInstance = null;
public static RedisClient GetInstance()
{
return defaultInstance ?? (defaultInstance = GetInstance("default"));
}
///
/// Static method for getting an instance.
/// This method will first check for named instances that has been set up programmatically.
/// If no such instance exists, it will check the "beitmemcached" section of the standard
/// config file and see if it can find configuration info for it there.
/// If that also fails, an exception is thrown.
///
/// The name of the instance.
/// The named instance.
public static RedisClient GetInstance(string name)
{
RedisClient c;
if (instances.TryGetValue(name, out c))
{
return c;
}
Error.Throw("Unable to find RedisClient instance \"" + name + "\".");
return null;
}
#endregion
#region Fields, constructors, and private methods.
public readonly string Name;
internal readonly ServerPool serverPool;
///
/// If you specify a key prefix, it will be appended to all keys before they are sent to the memcached server.
/// They key prefix is not used when calculating which server a key belongs to.
///
public string KeyPrefix { get { return keyPrefix; } set { keyPrefix = value; } }
private string keyPrefix = "";
///
/// The send receive timeout is used to determine how long the client should wait for data to be sent
/// and received from the server, specified in milliseconds. The default value is 2000.
///
public int SendReceiveTimeout { get { return serverPool.SendReceiveTimeout; } set { serverPool.SendReceiveTimeout = value; } }
///
/// The connect timeout is used to determine how long the client should wait for a connection to be established,
/// specified in milliseconds. The default value is 2000.
///
public int ConnectTimeout { get { return serverPool.ConnectTimeout; } set { serverPool.ConnectTimeout = value; } }
///
/// The min pool size determines the number of sockets the socket pool will keep.
/// Note that no sockets will be created on startup, only on use, so the socket pool will only
/// contain this amount of sockets if the amount of simultaneous requests goes above it.
/// The default value is 5.
///
public uint MinPoolSize
{
get { return serverPool.MinPoolSize; }
set
{
if (value > MaxPoolSize) { Error.Throw("MinPoolSize (" + value + ") may not be larger than the MaxPoolSize (" + MaxPoolSize + ")."); }
serverPool.MinPoolSize = value;
}
}
///
/// The max pool size determines how large the socket connection pool is allowed to grow.
/// There can be more sockets in use than this amount, but when the extra sockets are returned, they will be destroyed.
/// The default value is 10.
///
public uint MaxPoolSize
{
get { return serverPool.MaxPoolSize; }
set
{
if (value < MinPoolSize) { Error.Throw("MaxPoolSize (" + value + ") may not be smaller than the MinPoolSize (" + MinPoolSize + ")."); }
serverPool.MaxPoolSize = value;
}
}
///
/// If the pool contains more than the minimum amount of sockets, and a socket is returned that is older than this recycle age
/// that socket will be destroyed instead of put back in the pool. This allows the pool to shrink back to the min pool size after a peak in usage.
/// The default value is 30 minutes.
///
public TimeSpan SocketRecycleAge { get { return serverPool.SocketRecycleAge; } set { serverPool.SocketRecycleAge = value; } }
///
/// 指定数据长度超过值时进行压缩
///
private uint compressionThreshold = 1024 * 128; //128kb
///
/// If an object being stored is larger in bytes than the compression threshold, it will internally be compressed before begin stored,
/// and it will transparently be decompressed when retrieved. Only strings, byte arrays and objects can be compressed.
/// The default value is 1048576 bytes = 1MB.
///
public uint CompressionThreshold { get { return compressionThreshold; } set { compressionThreshold = value; } }
//Private constructor
private RedisClient(string name, string[] hosts)
{
if (String.IsNullOrEmpty(name))
{
Error.Throw("Name of RedisClient instance cannot be empty.");
}
if (hosts == null || hosts.Length == 0)
{
Error.Throw("Cannot configure RedisClient with empty list of hosts.");
}
Name = name;
serverPool = new ServerPool(hosts, CacheType.Redis);
serverPool.OnAuthEvent += new ServerPool.AuthDelegate(serverPool_OnAuthEvent);
}
bool serverPool_OnAuthEvent(MSocket socket)
{
if (!Auth(socket.SocketPool.password, socket))
{
Error.Throw("Auth password fail!");
}
return true;
}
///
/// Private key hashing method that uses the modified FNV hash.
///
/// The key to hash.
/// The hashed key.
private uint hash(string key)
{
checkKey(key);
return BitConverter.ToUInt32(new ModifiedFNV1_32().ComputeHash(Encoding.UTF8.GetBytes(key)), 0);
}
///
/// Private hashing method for user-supplied hash values.
///
/// The user-supplied hash value to hash.
/// The hashed value
private uint hash(uint hashvalue)
{
return BitConverter.ToUInt32(new ModifiedFNV1_32().ComputeHash(BitConverter.GetBytes(hashvalue)), 0);
}
///
/// Private multi-hashing method.
///
/// An array of keys to hash.
/// An arrays of hashes.
private uint[] hash(string[] keys)
{
uint[] result = new uint[keys.Length];
for (int i = 0; i < keys.Length; i++)
{
result[i] = hash(keys[i]);
}
return result;
}
///
/// Private multi-hashing method for user-supplied hash values.
///
/// An array of keys to hash.
/// An arrays of hashes.
private uint[] hash(uint[] hashvalues)
{
uint[] result = new uint[hashvalues.Length];
for (int i = 0; i < hashvalues.Length; i++)
{
result[i] = hash(hashvalues[i]);
}
return result;
}
///
/// Private key-checking method.
/// Throws an exception if the key does not conform to memcached protocol requirements:
/// It may not contain whitespace, it may not be null or empty, and it may not be longer than 250 characters.
///
/// The key to check.
private void checkKey(string key)
{
if (key == null)
{
throw new ArgumentNullException("Key may not be null.");
}
if (key.Length == 0)
{
throw new ArgumentException("Key may not be empty.");
}
if (key.Length > 250)
{
throw new ArgumentException("Key may not be longer than 250 characters.");
}
foreach (char c in key)
{
if (c <= 32)
{
throw new ArgumentException("Key may not contain whitespace or control characters.");
}
}
}
//Private Unix-time converter
private static DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static int getUnixTime(DateTime datetime)
{
return (int)(datetime.ToUniversalTime() - epoch).TotalSeconds;
}
#endregion
#region Set、Append
public void Append(string key, object value, int seconds) { Set("append", key, false, value, hash(key), seconds); }
public void Set(string key, object value, int seconds) { Set("set", key, false, value, hash(key), seconds); }
private bool Set(string command, string key, bool keyIsChecked, object value, uint hash, int expirySeconds)
{
if (!keyIsChecked)
{
checkKey(key);
}
string result = serverPool.Execute(hash, "", delegate(MSocket socket)
{
SerializedType type;
byte[] bytes;
byte[] typeBit = new byte[1];
try
{
bytes = Serializer.Serialize(value, out type, CompressionThreshold);
typeBit[0] = (byte)type;
}
catch (Exception e)
{
logger.Error("Error serializing object for key '" + key + "'.", e);
return "";
}
CheckDB(socket, hash);
using (RedisCommand cmd = new RedisCommand(socket, 3, command))
{
cmd.WriteKey(keyPrefix + key);
cmd.WriteValue(typeBit, bytes);
result = socket.ReadResponse();
if (result[0] != '-')
{
if (expirySeconds > 0)
{
cmd.Reset(3, "EXPIRE");
cmd.WriteKey(keyPrefix + key);
cmd.WriteValue(expirySeconds.ToString());
result = socket.ReadResponse();
}
}
return result;
}
});
return !string.IsNullOrEmpty(result);
}
#endregion
#region Get
public object Get(string key) { return Get("get", key, true, hash(key)); }
private object Get(string command, string key, bool keyIsChecked, uint hash)
{
if (!keyIsChecked)
{
checkKey(key);
}
object value = serverPool.Execute