1 redis单据锁

  1. <?php
  2. /**
  3. * 关键点:set()设置nx模式:单据key已存在时不再添加,返回false;
  4. * 生成唯一的value:保证在处理业务期间到提交事务,都是同一次单据();
  5. * 监听:其他连接源修改了该key(可能是并发,然后后者会提交,前者会丢弃),被修改了则丢弃事务,没有则提交。
  6. * Class LockService
  7. */
  8. class LockService
  9. {
  10. // 单据锁redis key模板(起始就是带前缀的键)
  11. const REDIS_LOCK_KEY_TEMPLATE = 'order_lock_%s';
  12. const REDIS_LOCK_DEFAULT_EXPIRE_TIME = 86400;
  13. const REDIS_CONFIG_HOST = '127.0.0.1';
  14. const REDIS_CONFIG_PORT = 6379;
  15. const REDIS_PASSWORD = 'root';
  16. //用于生成唯一的值(可理解为处理的某一类业务单据;用于处理多种类别时,应该区分开来。)
  17. const REDIS_LOCK_UNIQUE_ID_KEY = 'lock_unique_id';
  18. public static function getRedisConn($strIp = self::REDIS_CONFIG_HOST, $intPort = self::REDIS_CONFIG_PORT, $password = self::REDIS_PASSWORD)
  19. {
  20. $objRedis = new \Redis();
  21. $objRedis->connect($strIp, $intPort);
  22. // 我竟然不需要密码,本地的redis是配了密码的
  23. // $objRedis->auth('root');
  24. return $objRedis;
  25. }
  26. public static function generateUniqueLockId()
  27. {
  28. // 让该key上的值自增1;如果key不存在,如果key不存在,则先设定0,再自增1。所以,它是简易的获取唯一值
  29. return self::getRedisConn()->incr(self::REDIS_LOCK_UNIQUE_ID_KEY);
  30. }
  31. public static function addLock($intOrderId, $intExpireTime = self::REDIS_LOCK_DEFAULT_EXPIRE_TIME)
  32. {
  33. // 参数错误,加锁不成功
  34. if (empty($intOrderId || $intExpireTime <= 0)) {
  35. return false;
  36. }
  37. // 获取连接
  38. $objRedis = self::getRedisConn();
  39. //生成唯一锁ID,解锁需持有此ID
  40. $intUniqueLockId = self::generateUniqueLockId();
  41. //根据模板,结合单据ID,生成唯一Redis key(一般来说,单据ID在业务中系统中唯一的)
  42. $strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId);
  43. //加锁(通过Redis setnx指令实现,从Redis 2.6.12开始,通过set指令可选参数也可以实现setnx,同时可原子化地设置超时时间)
  44. // 第三个参数数值时,为过期时间;数组时,可以设置 赋值模式,过期时间;当前是不存在时才新建key和value
  45. $bolRes = $objRedis->set($strKey, $intUniqueLockId, ['nx', 'ex'=>$intExpireTime]);
  46. //加锁成功返回锁ID,加锁失败返回false
  47. return $bolRes ? $intUniqueLockId : $bolRes;
  48. }
  49. public static function releaseLock($intOrderId, $intLockId)
  50. {
  51. //参数校验
  52. if (empty($intOrderId) || empty($intLockId)) {
  53. return false;
  54. }
  55. //获取Redis连接
  56. $objRedisConn = self::getRedisConn();
  57. //生成Redis key
  58. $strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId);
  59. //监听Redis key防止在【比对lock id】与【解锁事务执行过程中】被修改或删除,提交事务后会自动取消监控,其他情况需手动解除监控
  60. // 且监控中的key,在事务执行期间,被其他connect修改了,则事务提交时会中断,不执行事务中的命令。
  61. $objRedisConn->watch($strKey);
  62. if ($intLockId == $objRedisConn->get($strKey)) {
  63. // multi开启事务(有序的执行命令的,Pipleline是无序的),exec提交事务,discard放弃事务
  64. $objRedisConn->multi()->del($strKey)->exec();
  65. return true;
  66. }
  67. // 解除监控
  68. $objRedisConn->unwatch();
  69. return false;
  70. }
  71. }
  72. //test
  73. $res1 = LockService::addLock('666666');
  74. var_dump($res1);//返回lock id,加锁成功
  75. $res2 = LockService::addLock('666666');
  76. var_dump($res2);//false,加锁失败
  77. $res3 = LockService::releaseLock('666666', $res1);
  78. var_dump($res3);//true,解锁成功
  79. $res4 = LockService::releaseLock('666666', $res1);
  80. var_dump($res4);//false,解锁失败
  • 可以在会产生多次回调到业务中考虑此方法。

2 JWT的生成与验证

<?php
class JWTHepler {
    private $header = [
        'typ' => 'JWT', // 标识整个token是一个jwt字符串
        'alg' => 'HS256', // 代表签名和摘要算法
    ];
    private $payload = [
        'sub' => '',
        'name' => '',
        'admin' => '',
    ];
    private $salt = "";

    public function setSalt(string $salt)
    {
        if(!isset($salt)) exit('please enter a salt');
        $this->salt = $salt;
        return $this;
    }

    public function setHeader(array $header)
    {
        if(!isset($header['typ'])) exit('please set the alg of header');
        $this->header = $header;
        return $this;
    }

    public function setPayload(array $payload)
    {
        $this->payload = $payload;
        return $this;
    }

    public function encript()
    {
        $base64Header = base64_encode(json_encode($this->header));
        $base64Payload = base64_encode(json_encode($this->payload));
        $secrity = "";
        switch(($this->header)['alg']) {
            case 'HS256':
                $secrity = hash_hmac('sha256', $base64Header . '.' . $base64Payload, $this->salt);
                echo $secrity . "\n";
                break;
            default: exit('there is not a func to create the secrity');break;
        }
        $result = $base64Header . '.' . $base64Payload . '.' . $secrity;
        return $result;
    }

    public function checkToken($token)
    {
        list($base64Header, $base64Payload, $secrity) = explode('.', $token);
        $header = json_decode(base64_decode($base64Header), true);
        $payload = json_decode(base64_decode($base64Payload), true);
        $tmpSecrity = "";

        switch($header['alg']) {
            case 'HS256':
                $tmpSecrity = hash_hmac('sha256', $base64Header . '.' . $base64Payload, $this->salt);
                break;
            default: exit('there is not a func to create the secrity');break;
        }
        if($tmpSecrity == $secrity) {
            echo "true\n";
            return [true, $header, $payload];
        } else {
            echo "false\n";
            return [false, [], []];
        }
    }

    public static function createSalt()
    {
        $str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        $salt = substr(str_shuffle($str), 26, rand(2, count_chars($str)));
        return $salt;
    }
}

$jwt = new JWTHepler();
$header = [
    'typ' => 'JWT',
    'alg' => 'HS256',
];
$data = [
    'sub' => uniqid("ABC"),
    'name' => 'test',
    'admin' => true,
];
// 生成token
$salt = JWTHepler::createSalt();
$token = $jwt->setHeader($header)->setPayload($data)->setSalt($salt)->encript();
echo $token . "\n";

// 验证token
$result = $jwt->checkToken($token);
var_dump($result);
  • 使用JWT可以解决分布式下,多个节点如何共享用户信息的问题。
  • 避免了对session、cookie的依赖。
  • 使用的时候,将JWT放在header里面:每次请求时,都携带它,后端验证正确后再执行后续操作。
  • 此处仅是一个简单的例子帮助理解,更好的操作方式可以下载专业的第三方依赖库。
  • token可以很容易的跨服务器,只要不同服务器实现相同解密算法即可;而cookie/.session是存在于某一台服务器上。
  • 参考各个主流网站的token过期时间,一般不超过1h.
  • token过期,就要重新获取。那么重新获取有两种方式,一是重复第一次获取token的过程(比如登录,扫描授权等),这样做的缺点是用户体验不好。第二种方法,主动去刷新token.主动刷新token的凭证是refresh_token,也是加密字符串,并且和token是相关联的。相比获取各种资源的token,refresh_token的作用仅仅是获取新的token,因此其作用和安全性要求都大为降低,所以其过期时间也可以设置得长一些。

    3 桶算法限流

    ```php <?php class Bucket { static $limits = []; // url => 次/分钟 static $times = []; // url => [窗口启动时间, 次/分钟]

    public static function setUrlLimit($url, $limit) {

      if(empty($url) || empty($limit)) {
          return false;
      }
      self::$limits[$url] = $limit;
      return true;
    

    }

    public static function removeUrlLimit($url) {

      unset($url);
      return true;
    

    }

    public static function checkRequest($url) {

      $limit = self::$limits[$url];
      if (!$limit) return true; // 0次或无设定,为无限制
    
      $nowTime= time();
      $times =self::$times[$url];
      if($times) {
          $startTime = $times['start_time'];
          $count = $times['count'];
          $interval = ($nowTime - $startTime) / 60;
          if($interval <= 1 && $count > 0) {
              self::$times[$url] = [
                  'start_time' => $nowTime,
                  'count' => $count - 1,
              ];
              return true;
          } else if($interval <= 1 && $count <= 0) {
              return false;
          }
      }
    
      // 还没记录过或重新补充令牌
      self::$times[$url] = [
          'start_time' => $nowTime,
          'count' => $limit - 1,
      ];
      return true;
    

    } }

Bucket::setUrlLimit(‘test/aa’, 5); Bucket::setUrlLimit(‘aa/bb’, 10);

for($i = 0; $i < 11; $i++) { $result1 = Bucket::checkRequest(“test/aa”); if($result1) { echo date(‘Y-m-d H:i:s’) . “|test/aa:do it” . “\n”; } else { echo date(‘Y-m-d H:i:s’) . “|test/aa:do not it” . “\n”; }

$result2 = Bucket::checkRequest("aa/bb");
if($result2) {
    echo date('Y-m-d H:i:s') . "|aa/bb:do it" . "\n";
} else {
    echo date('Y-m-d H:i:s') . "|aa/bb:do not it" . "\n";
}

}

sleep(61); echo “\n第二阶段\n”; for($i = 0; $i < 11; $i++) { $result1 = Bucket::checkRequest(“test/aa”); if($result1) { echo date(‘Y-m-d H:i:s’) . “|test/aa:do it” . “\n”; } else { echo date(‘Y-m-d H:i:s’) . “|test/aa:do not it” . “\n”; }

$result2 = Bucket::checkRequest("aa/bb");
if($result2) {
    echo date('Y-m-d H:i:s') . "|aa/bb:do it" . "\n";
} else {
    echo date('Y-m-d H:i:s') . "|aa/bb:do not it" . "\n";
}

} ```