言午月月鸟
编程,带娃以及思考人生
首页
编程
带娃
思考人生
编程画图秀
PHP更新缓存防击穿的一种方法
dingusxp
2740
**缓存击穿:** 网站高并发访问时,如果碰到热点缓存key失效,可能出现多个进程同时尝试更新缓存,集中访问数据库,造成数据库压力过大。 **对应方案:** 对更新缓存的操作加锁。 但PHP加锁不方便,实现时稍微变通下。 直接参见代码: ```php /** * 基于 redis 的 Cache 类 */ class RedisCache { // 默认重试取锁次数 (每次间隔 100ms) const DEFAULT_LOCK_RETRY = 10; /** * @var \Redis * Redis 实例 */ private $redis; /** * 缓存 key 前缀。如: cache: * @var type */ private $keyPrefix = 'cache:'; /** * remember 方法专用,一般默认即可 * 更新锁过期时间,即更新操作执行超时时间,单位:秒 * @var int */ private $lockerTTL = 5; /** * 构造函数 * @param array $option */ public function __construct(array $option = []) { if (isset($option['redis'])) { $this->setRedis($option['redis']); } if (isset($option['keyPrefix'])) { $this->setKeyPrefix($option['keyPrefix']); } if (isset($option['lockerTTL'])) { $this->setLockerTTL(intval($option['lockerTTL'])); } } /** * 设置 key 前缀 * @param type $keyPrefix */ public function setKeyPrefix($keyPrefix) { $this->keyPrefix = trim($keyPrefix); } /** * 设置 redis 实例 * @param \Redis $redis */ public function setRedis(\Redis $redis) { $this->redis = $redis; } // 设置更新锁过期时间 public function setLockerTTL($time) { $this->lockerTTL = $time; } // 数据 序列化、反序列化 private static function unserialize($content) { if (!$content) { return null; } return unserialize($content); } private static function serialize($data) { return serialize($data); } /** * 获取缓存key * @param type $key * @return type */ private function getCacheKey(string $key) { return $this->keyPrefix.trim($key); } private function getCacheKeysMapping(array $keys) { $mapping = []; foreach ($keys as $key) { $mapping[$key] = $this->getCacheKey($key); } return $mapping; } /** * 获取缓存 * @param string $key * @param type $default */ public function get(string $key, $default = null) { $value = $this->redis->get($this->getCacheKey($key)); return false === $value ? $default : self::unserialize($value); } /** * 批量获取缓存 * @param array $keys * @param type $default */ public function getMultiple(array $keys, $default = null) { if (!$keys) { return false; } $keysMapping = $this->getCacheKeysMapping($keys); $values = $this->redis->mGet(array_values($keysMapping)); $data = []; foreach ($keys as $idx => $key) { $data[$key] = false === $values[$idx] ? $default : self::unserialize($values[$idx]); } return $data; } /** * 设置缓存 * @param type $key * @param type $value * @param type $ttl */ public function set(string $key, $value, $ttl = null) { return $this->redis->set($this->getCacheKey($key), self::serialize($value), $ttl); } /** * 批量设置缓存 * @param array $values * @param type $ttl */ public function setMultiple(array $values, $ttl = null) { if (!$values) { return false; } // 开启 multi 方式 $multi = $this->redis->multi(); foreach ($values as $key => $value) { $multi->set($this->getCacheKey($key), self::serialize($value), $ttl); } $res = $multi->exec(); return array_combine(array_keys($values), array_values($res)); } /** * 删除指定缓存 * @param type $key */ public function delete(string $key) { return $this->redis->del($this->getCacheKey($key)); } /** * 批量删除指定缓存 * @param array $keys */ public function deleteMultiple(array $keys) { $keysMapping = $this->getCacheKeysMapping($keys); return $this->redis->del(array_values($keysMapping)); } /** * 清除全部缓存 * 无效:不允许该操作! */ public function clear() { // return $this->redis->flushDb(); return false; } /** * 检查缓存key是否存在 * @param type $key */ public function has(string $key) { return $this->redis->exists($this->getCacheKey($key)); } /** * 封装方法: 获取或更新缓存,有缓存更新的防击穿作用 * @param string $key * @param callable $callable * @param int $ttl * @param int $maxRetry */ public function remember(string $key, callable $callable, int $ttl, ?int $maxRetry = null) { // 取缓存 $missValue = 'random miss value #'.mt_rand(10000000, 99999999); $cacheData = $this->get($key, $missValue); // 命中,直接返回 if ($missValue !== $cacheData) { return $cacheData; } // 缓存更新 // 使用 setnx 操作取锁 $lockKey = $this->getCacheKey($key . '-update_lock'); $check = $this->redis->set($lockKey, 1, ['nx', 'ex' => $this->lockerTTL]); // 如果操作失败(已经有其它请求去做更新了) if (!$check) { // 判断是否继续取锁 if (null === $maxRetry) { $maxRetry = self::DEFAULT_LOCK_RETRY; } $maxRetry--; if ($maxRetry < 0) { return false; } // 稍作休息,继续重试获取数据 usleep(100); return $this->remember($key, $callable, $ttl, $maxRetry); } // 调用用户函数实际获取数据 try { $data = call_user_func($callable); } catch (Exception $e) { // 获取数据失败也写缓存并设置一个较短的过期时间(防缓存穿透) $data = null; $ttl = $this->lockerTTL; } // 设置的过期时间要高于预期过期时间 $this->set($key, $data, $ttl); return $data; } } ``` 测试代码: ```PHP // test.php function test_remember($id) { $redis = new Redis(); $redis->connect('127.0.0.1'); $cacher = new RedisCache(['redis' => $redis]); $cacheKey = 'test_key:'.$id; $param = ['name' => 'tester #'.$id]; echo $cacher->remember($cacheKey, function() use($param) { file_put_contents('/tmp/test.log', date('[H:i:s] ').'miss: '.$param['name'].PHP_EOL, FILE_APPEND); return 'hello, '.$param['name'].'. -- set at '.date('H:i:s'); }, 6); } $id = intval(time() / 10); echo '#', $id, ' => ', test_remember($id), PHP_EOL; ``` 测试: ```PHP # 直接php开启内置server (当然你也可以用 nginx + php-fpm 测试) php -S 127.0.0.1:8880 # 用 ab 发送并发请求 ab -t 30 -c 10 http://127.0.0.1:8880/test.php # ab运行中,同时可以自己 curl 查看请求结果是否符合预期 curl http://127.0.0.1:8880/test.php # 查看 /tmp/test.log 日志,观察缓存更新情况(理论上更新次数应该小于6次) tail -n 10 /tmp/test.log ```
粤ICP备19051469号-1
Copyright©dingusxp.com - All Rights Reserved
Template by
OS Templates