Redis缓存/NoSQL使用经验笔记,分布式锁

4 篇文章 0 订阅
3 篇文章 0 订阅

背景:

本人主要在php项目中使用redis,使用场合包括:

  1. 缓存作用,对于需要被频繁读取,但是更新不频繁的数据,在更新数据库数据后更新缓存,读取数据从缓存读取。
  2. NoSQL作用,开发过java web项目再做php开发的童鞋肯能会遇到一个困惑,比如想实现统计网站访问次数,java可以用application超全局变量实现,而php-fpm似乎不存在比SESSION作用域更广的超全局变量,所以我只能用redis来充当这个角色。
  3. 共享型的分布式SESSION,在做分布式或者集群时,比较费时费力的方式是多主机同步session,高明点的是使用缓存实现共享的SESSION(运算节点不存储任何数据)或者jwt无状态(笔者觉得jwt尤其局限性,并且redis可以使用cluster,不会轻易成为瓶颈,所以主张用redis实现分布式session)

题外话,大家都知道redis会定时进行持久化,相比于memchace,redis虽然性能稍低一点(?),但是数据不易失。前些日子服务器欠费停机,重新启动服务器,发现redis缓存还原了几个月前的内容,最近的数据呢? (WTF ?!!!),当时就蒙了,然而再重启一遍redis,竟然奇迹般地恢复了最新的数据,正常了!这种现象是为什么呢?

经验和教训

  1. 关于并发锁,本来想着基于缓存的业务实现,速度很快应该不需要并发锁,但是在网络极端的情况下,没这个会出现脏读错误。如:有一个基于ajax请求的确定按钮,请求结果返回后弹窗提示成功,但是后来出问题了,后端出现了重复操作,导致了不一致。推测是网络卡了,用户狂点确定按钮,以至于十几个请求几乎同时到达服务端,而服务端操作包括读取redis数据,计算,写入redis,还没写入,下一个请求便读取了数据,出现了脏读。
  2. 作为nosql数据粒度要合适,粒度过大,上线久了,一个key存储的数据变得庞大,会影响性能,而且redis集群,是根据key的一致性hash值来分槽的,单个key太大也不利于均匀分配负载。
  3. 作为缓存,需要良定义,做一定的封装,需要明确加载缓存时机,懒加载还是主动更新,懒加载如何标记为需要加载,还需要明确定义数据生命周期,避免垃圾越积越多,避免缓存穿透。

分布式锁

  1. 要说锁,不得不谈事务,不得不谈事务的一些基础知识:
  • 事务的四个性质(ACID):原子性(atomicity),一致性(consistency),隔离性(isolation),持久性(durability)

  • 数据库四个隔离级别:Read uncommitted 、Read committed 、Repeatable read 、Serializable

  • 常见的锁:共享锁,更新锁,排他锁,悲观锁,乐观锁
    关于这些概念,可参考文章Redis的事务功能详解
    Redis有批量处理的机制:

MULTI 、 EXECDISCARD 和 WATCH 

一般情况下,这个几个命令可以实现操作的原子性,WATCH锁为乐观锁,Redis事务足够使用了,详细可以参考Redis 事务这篇文章,可以让这几个命令的批量执行,如果这个批量执行有读有些,写操作依赖于读的结果,这个操作就无法实现。
2. Redis可以执行Lua脚本实现批量执行:
一定程度上可以解决1中的问题,但是在实现具体业务逻辑时,就会发现程序设计和编写困难。Lua可以运行Lua脚本实现批量执行,一定成都上可以解决1中的问题,但是在实现具体业务逻辑时,就会发现程序设计和编写困难。
3. 分布式锁:

  • 为什么需要分布式锁: 一个函数可能要读取Redis缓存里的内容,修改后再写入Redis缓存,在网络极端或者高并发的情况下,两个请求同时到达,会出现第一个请求读取数据后尚未写入,第二个请求也读取了数据,也就是幻读,这种幻读会导致数据的不一致,因此需要实现分布式锁,实现在第一个请求写入缓存后,才允许第二个请求读取。
  • 构造锁的时候,我们需要注意几个问题:

1、预防处理持有锁在执行操作的时候进程奔溃,导致死锁,其他进程一直得不到此锁
2、持有锁进程因为操作时间长而导致锁自动释放,但本身进程并不知道,最后错误的释放其他进程的锁
3、一个进程锁过期后,其他多个进程同时尝试获取锁,并且都成功获得锁

  1. 推荐一个优良的php实现 :
<?php
#分布式锁
class Lock
{
 private $redis=''; #存储redis对象
 /**
 * @desc 构造函数
 * 
 * @param $host string | redis主机
 * @param $port int | 端口
 */
 public function __construct($host,$port=6379)
 {
  $this->redis=new Redis();
  $this->redis->connect($host,$port);
 } 
 /**
 * @desc 加锁方法
 *
 * @param $lockName string | 锁的名字
 * @param $timeout int | 锁的过期时间
 *
 * @return 成功返回identifier/失败返回false
 */
 public function getLock($lockName, $timeout=2)
 {
  $identifier=uniqid();  #获取唯一标识符
  $timeout=ceil($timeout); #确保是整数
  $end=time()+$timeout;
  while(time()<$end)   #循环获取锁
  {
   if($this->redis->setnx($lockName, $identifier)) #查看$lockName是否被上锁
   {
    $this->redis->expire($lockName, $timeout);  #为$lockName设置过期时间,防止死锁
    return $identifier;        #返回一维标识符
   }
   elseif ($this->redis->ttl($lockName)===-1) 
   {                        
    $this->redis->expire($lockName, $timeout);  #检测是否有设置过期时间,没有则加上(假设,客户端A上一步没能设置时间就进程奔溃了,客户端B就可检测出来,并设置时间)
   }
   usleep(0.001);   #停止0.001ms
  }
  return false;
 }
 /**
 * @desc 释放锁
 *
 * @param $lockName string | 锁名
 * @param $identifier string | 锁的唯一值
 *
 * @param bool
 */
 public function releaseLock($lockName,$identifier)
 {
  if($this->redis->get($lockName)==$identifier) #判断是锁有没有被其他客户端修改
  { 
   $this->redis->multi();
   $this->redis->del($lockName); #释放锁
   $this->redis->exec();
   return true;
  }
  else
  {
   return false; #其他客户端修改了锁,不能删除别人的锁
  }
 }
 /**
 * @desc 测试
 * 
 * @param $lockName string | 锁名
 */
 public function test($lockName)
 {
  $start=time();
  for ($i=0; $i < 10000; $i++) 
  { 
   $identifier=$this->getLock($lockName);
   if($identifier)
   {
    $count=$this->redis->get('count');
    $count=$count+1;
    $this->redis->set('count',$count);
    $this->releaseLock($lockName,$identifier);
   } 
  }
  $end=time();
  echo "this OK<br/>";
  echo "执行时间为:".($end-$start);
 }
 
}
header("content-type: text/html;charset=utf8;");
$obj=new Lock('192.168.95.11');
$obj->test('lock_count');
?>

这个实现的优点另外创建键值对,无需调整原有数据结构,缺点是效率低,相当于Serializable级别的隔离,在锁未释放的情况下,另外的进程会无法读取。
参考文章:
https://blog.csdn.net/ypp91zr/article/details/69487648
https://www.jb51.net/article/109704.htm
https://www.cnblogs.com/tian666/p/7852646.html


2019年8月5日更新

  • 缓存的应用应当是:
    1. 仅仅实现懒加载的情形下
      1. 从缓存中读取,读不到了从硬盘中读取写入到缓存,并返回。
      2. 更新(包括删除)操作对硬盘内容进行更新,并删除对应的缓存。
      3. 这里的重点是如何建立好一对一的操作,多对一的操作,一对多的操作。
    2. 上述设计中的重点难点
      1. 一对一的设计,对于每一个不同的具有不同参数的检索建立缓存,如何让更新操作可以命中非单一记录的查询,似乎需要在缓存总建立主键到查询的索引(一对多映射)。
      2. 多对一,多个查询命中的是同一个数据实体时,如何有效的降低内存资源消耗。
      3. 要有合适的超时淘汰策略,需要考虑数据的生命周期。
    3. 我所采用的思路看似愚蠢繁琐,但是没上述问题
      1. 直接把缓存当做k-v数据库来用,在合适的时间节点跟关系型数据库进行同步。
      2. 很多更新只做热更新,只在业务到达了里程碑才更新到关系型数据库中,缓存命中率高到不能再高了,空间利用效率也相当好。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值