背景:
本人主要在php项目中使用redis,使用场合包括:
- 缓存作用,对于需要被频繁读取,但是更新不频繁的数据,在更新数据库数据后更新缓存,读取数据从缓存读取。
- NoSQL作用,开发过java web项目再做php开发的童鞋肯能会遇到一个困惑,比如想实现统计网站访问次数,java可以用application超全局变量实现,而php-fpm似乎不存在比SESSION作用域更广的超全局变量,所以我只能用redis来充当这个角色。
- 共享型的分布式SESSION,在做分布式或者集群时,比较费时费力的方式是多主机同步session,高明点的是使用缓存实现共享的SESSION(运算节点不存储任何数据)或者jwt无状态(笔者觉得jwt尤其局限性,并且redis可以使用cluster,不会轻易成为瓶颈,所以主张用redis实现分布式session)
题外话,大家都知道redis会定时进行持久化,相比于memchace,redis虽然性能稍低一点(?),但是数据不易失。前些日子服务器欠费停机,重新启动服务器,发现redis缓存还原了几个月前的内容,最近的数据呢? (WTF ?!!!),当时就蒙了,然而再重启一遍redis,竟然奇迹般地恢复了最新的数据,正常了!这种现象是为什么呢?
经验和教训
- 关于并发锁,本来想着基于缓存的业务实现,速度很快应该不需要并发锁,但是在网络极端的情况下,没这个会出现脏读错误。如:有一个基于ajax请求的确定按钮,请求结果返回后弹窗提示成功,但是后来出问题了,后端出现了重复操作,导致了不一致。推测是网络卡了,用户狂点确定按钮,以至于十几个请求几乎同时到达服务端,而服务端操作包括读取redis数据,计算,写入redis,还没写入,下一个请求便读取了数据,出现了脏读。
- 作为nosql数据粒度要合适,粒度过大,上线久了,一个key存储的数据变得庞大,会影响性能,而且redis集群,是根据key的一致性hash值来分槽的,单个key太大也不利于均匀分配负载。
- 作为缓存,需要良定义,做一定的封装,需要明确加载缓存时机,懒加载还是主动更新,懒加载如何标记为需要加载,还需要明确定义数据生命周期,避免垃圾越积越多,避免缓存穿透。
分布式锁
- 要说锁,不得不谈事务,不得不谈事务的一些基础知识:
-
事务的四个性质(ACID):原子性(atomicity),一致性(consistency),隔离性(isolation),持久性(durability)
-
数据库四个隔离级别:Read uncommitted 、Read committed 、Repeatable read 、Serializable
-
常见的锁:共享锁,更新锁,排他锁,悲观锁,乐观锁
关于这些概念,可参考文章Redis的事务功能详解
Redis有批量处理的机制:
MULTI 、 EXEC 、 DISCARD 和 WATCH
一般情况下,这个几个命令可以实现操作的原子性,WATCH锁为乐观锁,Redis事务足够使用了,详细可以参考Redis 事务这篇文章,可以让这几个命令的批量执行,如果这个批量执行有读有些,写操作依赖于读的结果,这个操作就无法实现。
2. Redis可以执行Lua脚本实现批量执行:
一定程度上可以解决1中的问题,但是在实现具体业务逻辑时,就会发现程序设计和编写困难。Lua可以运行Lua脚本实现批量执行,一定成都上可以解决1中的问题,但是在实现具体业务逻辑时,就会发现程序设计和编写困难。
3. 分布式锁:
- 为什么需要分布式锁: 一个函数可能要读取Redis缓存里的内容,修改后再写入Redis缓存,在网络极端或者高并发的情况下,两个请求同时到达,会出现第一个请求读取数据后尚未写入,第二个请求也读取了数据,也就是幻读,这种幻读会导致数据的不一致,因此需要实现分布式锁,实现在第一个请求写入缓存后,才允许第二个请求读取。
- 构造锁的时候,我们需要注意几个问题:
1、预防处理持有锁在执行操作的时候进程奔溃,导致死锁,其他进程一直得不到此锁
2、持有锁进程因为操作时间长而导致锁自动释放,但本身进程并不知道,最后错误的释放其他进程的锁
3、一个进程锁过期后,其他多个进程同时尝试获取锁,并且都成功获得锁
- 推荐一个优良的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日更新
- 缓存的应用应当是:
- 仅仅实现懒加载的情形下
- 从缓存中读取,读不到了从硬盘中读取写入到缓存,并返回。
- 更新(包括删除)操作对硬盘内容进行更新,并删除对应的缓存。
- 这里的重点是如何建立好一对一的操作,多对一的操作,一对多的操作。
- 上述设计中的重点难点
- 一对一的设计,对于每一个不同的具有不同参数的检索建立缓存,如何让更新操作可以命中非单一记录的查询,似乎需要在缓存总建立主键到查询的索引(一对多映射)。
- 多对一,多个查询命中的是同一个数据实体时,如何有效的降低内存资源消耗。
- 要有合适的超时淘汰策略,需要考虑数据的生命周期。
- 我所采用的思路看似愚蠢繁琐,但是没上述问题
- 直接把缓存当做k-v数据库来用,在合适的时间节点跟关系型数据库进行同步。
- 很多更新只做热更新,只在业务到达了里程碑才更新到关系型数据库中,缓存命中率高到不能再高了,空间利用效率也相当好。
- 仅仅实现懒加载的情形下