// +---------------------------------------------------------------------- namespace think\cache\driver; use think\Cache; /** * Secache缓存驱动 * @author liu21st */ class Secache { protected $handler = null; protected $options = [ 'project' => '', 'path' => '', 'expire' => 0, 'prefix' => '', ]; /** * 架构函数 * @param array $options 缓存参数 * @access public */ public function __construct($options = []) { if (!empty($options)) { $this->options = array_merge($this->options, $options); } if (substr($this->options['path'], -1) != '/') { $this->options['path'] .= '/'; } $this->handler = new SecacheClient; $this->handler->workat($this->options['path'] . $this->options['project']); } /** * 读取缓存 * @access public * @param string $name 缓存变量名 * @return mixed */ public function get($name) { $name = $this->options['prefix'] . $name; $key = md5($name); $this->handler->fetch($key, $return); return $return; } /** * 写入缓存 * @access public * @param string $name 缓存变量名 * @param mixed $value 存储数据 * @param integer $expire 有效时间(秒) * @return boolean */ public function set($name, $value) { $name = $this->options['prefix'] . $name; $key = md5($name); return $this->handler->store($key, $value); } /** * 删除缓存 * @access public * @param string $name 缓存变量名 * @return boolen */ public function rm($name) { $name = $this->options['prefix'] . $name; $key = md5($name); return $this->handler->delete($key); } /** * 清除缓存 * @access public * @return boolen */ public function clear() { return $this->handler->_format(true); } } if (!defined('SECACHE_SIZE')) { define('SECACHE_SIZE', '15M'); } class SecacheClient { public $idx_node_size = 40; public $data_base_pos = 262588; //40+20+24*16+16*16*16*16*4; public $schema_item_size = 24; public $header_padding = 20; //保留空间 放置php标记防止下载 public $info_size = 20; //保留空间 4+16 maxsize|ver //40起 添加20字节保留区域 public $idx_seq_pos = 40; //id 计数器节点地址 public $dfile_cur_pos = 44; //id 计数器节点地址 public $idx_free_pos = 48; //id 空闲链表入口地址 public $idx_base_pos = 444; //40+20+24*16 public $min_size = 10240; //10M最小值 public $schema_struct = array('size', 'free', 'lru_head', 'lru_tail', 'hits', 'miss'); public $ver = '$Rev: 3 $'; public $name = '系统默认缓存(文件型)'; public function workat($file) { $this->_file = $file . '.php'; $this->_bsize_list = array( 512 => 10, 3 << 10 => 10, 8 << 10 => 10, 20 << 10 => 4, 30 << 10 => 2, 50 << 10 => 2, 80 << 10 => 2, 96 << 10 => 2, 128 << 10 => 2, 224 << 10 => 2, 256 << 10 => 2, 512 << 10 => 1, 1024 << 10 => 1, ); $this->_node_struct = array( 'next' => array(0, 'V'), 'prev' => array(4, 'V'), 'data' => array(8, 'V'), 'size' => array(12, 'V'), 'lru_right' => array(16, 'V'), 'lru_left' => array(20, 'V'), 'key' => array(24, 'H*'), ); if (!file_exists($this->_file)) { $this->create(); } else { $this->_rs = fopen($this->_file, 'rb+') || $this->triggerError('Can\'t open the cachefile: ' . realpath($this->_file), E_USER_ERROR); $this->_seek($this->header_padding); $info = unpack('V1max_size/a*ver', fread($this->_rs, $this->info_size)); if ($info['ver'] != $this->ver) { $this->_format(true); } else { $this->max_size = $info['max_size']; } } $this->idx_node_base = $this->data_base_pos + $this->max_size; $this->_block_size_list = array_keys($this->_bsize_list); sort($this->_block_size_list); return true; } public function create() { $this->_rs = fopen($this->_file, 'wb+') || $this->triggerError('Can\'t open the cachefile: ' . realpath($this->_file), E_USER_ERROR); fseek($this->_rs, 0); fputs($this->_rs, '<' . '?php exit()?' . '>'); return $this->_format(); } public function _puts($offset, $data) { if ($offset < $this->max_size * 1.5) { $this->_seek($offset); return fputs($this->_rs, $data); } else { $this->triggerError('Offset over quota:' . $offset, E_USER_ERROR); } } public function _seek($offset) { return fseek($this->_rs, $offset); } public function clear() { return $this->_format(true); } public function fetch($key, &$return) { $locked = false; if ($this->lock(false)) { $locked = true; } if ($this->search($key, $offset)) { $info = $this->_get_node($offset); $schema_id = $this->_get_size_schema_id($info['size']); if (false === $schema_id) { if ($locked) { $this->unlock(); } return false; } $this->_seek($info['data']); $data = fread($this->_rs, $info['size']); $return = unserialize($data); if (false === $return) { if ($locked) { $this->unlock(); } return false; } if ($locked) { $this->_lru_push($schema_id, $info['offset']); $this->_set_schema($schema_id, 'hits', $this->_get_schema($schema_id, 'hits') + 1); return $this->unlock(); } else { return true; } } else { if ($locked) { $this->unlock(); } return false; } } /** * lock * 如果flock不管用,请继承本类,并重载此方法 * * @param mixed $is_block 是否阻塞 * @access public * @return bool */ public function lock($is_block, $whatever = false) { return flock($this->_rs, $is_block ? LOCK_EX : LOCK_EX + LOCK_NB); } /** * unlock * 如果flock不管用,请继承本类,并重载此方法 * * @access public * @return bool */ public function unlock() { return flock($this->_rs, LOCK_UN); } public function delete($key, $pos = false) { if ($pos || $this->search($key, $pos)) { if ($info = $this->_get_node($pos)) { //删除data区域 if ($info['prev']) { $this->_set_node($info['prev'], 'next', $info['next']); $this->_set_node($info['next'], 'prev', $info['prev']); } else { //改入口位置 $this->_set_node($info['next'], 'prev', 0); $this->_set_node_root($key, $info['next']); } $this->_free_dspace($info['size'], $info['data']); $this->_lru_delete($info); $this->_free_node($pos); return $info['prev']; } } return false; } public function store($key, $value) { if ($this->lock(true)) { //save data $data = serialize($value); $size = strlen($data); //get list_idx $has_key = $this->search($key, $list_idx_offset); $schema_id = $this->_get_size_schema_id($size); if (false === $schema_id) { $this->unlock(); return false; } if ($has_key) { $hdseq = $list_idx_offset; $info = $this->_get_node($hdseq); if ($this->_get_size_schema_id($info['size']) == $schema_id) { $dataoffset = $info['data']; } else { //破掉原有lru $this->_lru_delete($info); if (!($dataoffset = $this->_dalloc($schema_id))) { $this->unlock(); return false; } $this->_free_dspace($info['size'], $info['data']); $this->_set_node($hdseq, 'lru_left', 0); $this->_set_node($hdseq, 'lru_right', 0); } $this->_set_node($hdseq, 'size', $size); $this->_set_node($hdseq, 'data', $dataoffset); } else { if (!($dataoffset = $this->_dalloc($schema_id))) { $this->unlock(); return false; } $hdseq = $this->_alloc_idx(array( 'next' => 0, 'prev' => $list_idx_offset, 'data' => $dataoffset, 'size' => $size, 'lru_right' => 0, 'lru_left' => 0, 'key' => $key, )); if ($list_idx_offset > 0) { $this->_set_node($list_idx_offset, 'next', $hdseq); } else { $this->_set_node_root($key, $hdseq); } } if ($dataoffset > $this->max_size) { $this->triggerError('alloc datasize:' . $dataoffset, E_USER_WARNING); return false; } $this->_puts($dataoffset, $data); $this->_set_schema($schema_id, 'miss', $this->_get_schema($schema_id, 'miss') + 1); $this->_lru_push($schema_id, $hdseq); $this->unlock(); return true; } else { $this->triggerError("Couldn't lock the file !", E_USER_WARNING); return false; } } /** * search * 查找指定的key * 如果找到节点则$pos=节点本身 返回true * 否则 $pos=树的末端 返回false * * @param mixed $key * @access public * @return mixed */ public function search($key, &$pos) { return $this->_get_pos_by_key($this->_get_node_root($key), $key, $pos); } public function _get_size_schema_id($size) { foreach ($this->_block_size_list as $k => $block_size) { if ($size <= $block_size) { return $k; } } return false; } public function _parse_str_size($str_size, $default) { if (preg_match('/^([0-9]+)\s*([gmk]|)$/i', $str_size, $match)) { switch (strtolower($match[2])) { case 'g': if ($match[1] > 1) { $this->triggerError('Max cache size 1G', E_USER_ERROR); } $size = $match[1] << 30; break; case 'm': $size = $match[1] << 20; break; case 'k': $size = $match[1] << 10; break; default: $size = $match[1]; } if ($size <= 0) { $this->triggerError('Error cache size ' . $this->max_size, E_USER_ERROR); return false; } elseif ($size < 10485760) { return 10485760; } else { return $size; } } else { return $default; } } public function _format($truncate = false) { if ($this->lock(true, true)) { if ($truncate) { $this->_seek(0); ftruncate($this->_rs, $this->idx_node_base); } $this->max_size = $this->_parse_str_size(SECACHE_SIZE, 15728640); //default:15m $this->_puts($this->header_padding, pack('V1a*', $this->max_size, $this->ver)); ksort($this->_bsize_list); $ds_offset = $this->data_base_pos; $i = 0; foreach ($this->_bsize_list as $size => $count) { //将预分配的空间注册到free链表里 $count *= min(3, floor($this->max_size / 10485760)); $next_free_node = 0; for ($j = 0; $j < $count; $j++) { $this->_puts($ds_offset, pack('V', $next_free_node)); $next_free_node = $ds_offset; $ds_offset += intval($size); } $code = pack(str_repeat('V1', count($this->schema_struct)), $size, $next_free_node, 0, 0, 0, 0); $this->_puts(60 + $i * $this->schema_item_size, $code); $i++; } $this->_set_dcur_pos($ds_offset); $this->_puts($this->idx_base_pos, str_repeat("\0", 262144)); $this->_puts($this->idx_seq_pos, pack('V', 1)); $this->unlock(); return true; } else { $this->triggerError("Couldn't lock the file !", E_USER_ERROR); return false; } } public function _get_node_root($key) { $this->_seek(hexdec(substr($key, 0, 4)) * 4 + $this->idx_base_pos); $a = fread($this->_rs, 4); list(, $offset) = unpack('V', $a); return $offset; } public function _set_node_root($key, $value) { return $this->_puts(hexdec(substr($key, 0, 4)) * 4 + $this->idx_base_pos, pack('V', $value)); } public function _set_node($pos, $key, $value) { if (!$pos) { return false; } if (isset($this->_node_struct[$key])) { return $this->_puts($pos * $this->idx_node_size + $this->idx_node_base + $this->_node_struct[$key][0], pack($this->_node_struct[$key][1], $value)); } else { return false; } } public function _get_pos_by_key($offset, $key, &$pos) { if (!$offset) { $pos = 0; return false; } $info = $this->_get_node($offset); if ($info['key'] == $key) { $pos = $info['offset']; return true; } elseif ($info['next'] && $info['next'] != $offset) { return $this->_get_pos_by_key($info['next'], $key, $pos); } else { $pos = $offset; return false; } } public function _lru_delete($info) { if ($info['lru_right']) { $this->_set_node($info['lru_right'], 'lru_left', $info['lru_left']); } else { $this->_set_schema($this->_get_size_schema_id($info['size']), 'lru_tail', $info['lru_left']); } if ($info['lru_left']) { $this->_set_node($info['lru_left'], 'lru_right', $info['lru_right']); } else { $this->_set_schema($this->_get_size_schema_id($info['size']), 'lru_head', $info['lru_right']); } return true; } public function _lru_push($schema_id, $offset) { $lru_head = $this->_get_schema($schema_id, 'lru_head'); $lru_tail = $this->_get_schema($schema_id, 'lru_tail'); if ((!$offset) || ($lru_head == $offset)) { return; } $info = $this->_get_node($offset); $this->_set_node($info['lru_right'], 'lru_left', $info['lru_left']); $this->_set_node($info['lru_left'], 'lru_right', $info['lru_right']); $this->_set_node($offset, 'lru_right', $lru_head); $this->_set_node($offset, 'lru_left', 0); $this->_set_node($lru_head, 'lru_left', $offset); $this->_set_schema($schema_id, 'lru_head', $offset); if (0 == $lru_tail) { $this->_set_schema($schema_id, 'lru_tail', $offset); } elseif ($lru_tail == $offset && $info['lru_left']) { $this->_set_schema($schema_id, 'lru_tail', $info['lru_left']); } return true; } public function _get_node($offset) { $this->_seek($offset * $this->idx_node_size + $this->idx_node_base); $info = unpack('V1next/V1prev/V1data/V1size/V1lru_right/V1lru_left/H*key', fread($this->_rs, $this->idx_node_size)); $info['offset'] = $offset; return $info; } public function _lru_pop($schema_id) { if ($node = $this->_get_schema($schema_id, 'lru_tail')) { $info = $this->_get_node($node); if (!$info['data']) { return false; } $this->delete($info['key'], $info['offset']); if (!$this->_get_schema($schema_id, 'free')) { $this->triggerError('pop lru,But nothing free...', E_USER_ERROR); } return $info; } else { return false; } } public function _dalloc($schema_id, $lru_freed = false) { if ($free = $this->_get_schema($schema_id, 'free')) { //如果lru里有链表 $this->_seek($free); list(, $next) = unpack('V', fread($this->_rs, 4)); $this->_set_schema($schema_id, 'free', $next); return $free; } elseif ($lru_freed) { $this->triggerError('Bat lru poped freesize', E_USER_ERROR); return false; } else { $ds_offset = $this->_get_dcur_pos(); $size = $this->_get_schema($schema_id, 'size'); if ($size + $ds_offset > $this->max_size) { if ($info = $this->_lru_pop($schema_id)) { return $this->_dalloc($schema_id, $info); } else { $this->triggerError('Can\'t alloc dataspace', E_USER_ERROR); return false; } } else { $this->_set_dcur_pos($ds_offset + $size); return $ds_offset; } } } public function _get_dcur_pos() { $this->_seek($this->dfile_cur_pos); list(, $ds_offset) = unpack('V', fread($this->_rs, 4)); return $ds_offset; } public function _set_dcur_pos($pos) { return $this->_puts($this->dfile_cur_pos, pack('V', $pos)); } public function _free_dspace($size, $pos) { if ($pos > $this->max_size) { $this->triggerError('free dspace over quota:' . $pos, E_USER_ERROR); return false; } $schema_id = $this->_get_size_schema_id($size); if ($free = $this->_get_schema($schema_id, 'free')) { $this->_puts($free, pack('V1', $pos)); } else { $this->_set_schema($schema_id, 'free', $pos); } $this->_puts($pos, pack('V1', 0)); } public function _dfollow($pos, &$c) { $c++; $this->_seek($pos); list(, $next) = unpack('V1', fread($this->_rs, 4)); if ($next) { return $this->_dfollow($next, $c); } else { return $pos; } } public function _free_node($pos) { $this->_seek($this->idx_free_pos); list(, $prev_free_node) = unpack('V', fread($this->_rs, 4)); $this->_puts($pos * $this->idx_node_size + $this->idx_node_base, pack('V', $prev_free_node) . str_repeat("\0", $this->idx_node_size - 4)); return $this->_puts($this->idx_free_pos, pack('V', $pos)); } public function _alloc_idx($data) { $this->_seek($this->idx_free_pos); list(, $list_pos) = unpack('V', fread($this->_rs, 4)); if ($list_pos) { $this->_seek($list_pos * $this->idx_node_size + $this->idx_node_base); list(, $prev_free_node) = unpack('V', fread($this->_rs, 4)); $this->_puts($this->idx_free_pos, pack('V', $prev_free_node)); } else { $this->_seek($this->idx_seq_pos); list(, $list_pos) = unpack('V', fread($this->_rs, 4)); $this->_puts($this->idx_seq_pos, pack('V', $list_pos + 1)); } return $this->_create_node($list_pos, $data); } public function _create_node($pos, $data) { $this->_puts($pos * $this->idx_node_size + $this->idx_node_base , pack('V1V1V1V1V1V1H*', $data['next'], $data['prev'], $data['data'], $data['size'], $data['lru_right'], $data['lru_left'], $data['key'])); return $pos; } public function _set_schema($schema_id, $key, $value) { $info = array_flip($this->schema_struct); return $this->_puts(60 + $schema_id * $this->schema_item_size + $info[$key] * 4, pack('V', $value)); } public function _get_schema($id, $key) { $info = array_flip($this->schema_struct); $this->_seek(60 + $id * $this->schema_item_size); unpack('V1' . implode('/V1', $this->schema_struct), fread($this->_rs, $this->schema_item_size)); $this->_seek(60 + $id * $this->schema_item_size + $info[$key] * 4); list(, $value) = unpack('V', fread($this->_rs, 4)); return $value; } public function _all_schemas() { $schema = []; for ($i = 0; $i < 16; $i++) { $this->_seek(60 + $i * $this->schema_item_size); $info = unpack('V1' . implode('/V1', $this->schema_struct), fread($this->_rs, $this->schema_item_size)); if ($info['size']) { $info['id'] = $i; $schema[$i] = $info; } else { return $schema; } } } public function schemaStatus() { $return = []; foreach ($this->_all_schemas() as $k => $schemaItem) { if ($schemaItem['free']) { $this->_dfollow($schemaItem['free'], $schemaItem['freecount']); } $return[] = $schemaItem; } return $return; } public function status(&$curBytes, &$totalBytes) { $totalBytes = $curBytes = 0; $hits = $miss = 0; $schemaStatus = $this->schemaStatus(); $totalBytes = $this->max_size; $freeBytes = $this->max_size - $this->_get_dcur_pos(); foreach ($schemaStatus as $schema) { $freeBytes += $schema['freecount'] * $schema['size']; $miss += $schema['miss']; $hits += $schema['hits']; } $curBytes = $totalBytes - $freeBytes; $return[] = array('name' => '缓存命中', 'value' => $hits); $return[] = array('name' => '缓存未命中', 'value' => $miss); return $return; } public function triggerError($errstr, $errno) { triggerError($errstr, $errno); } }