高并发的一些处理意见
高并发一般指的是,同一时刻有许多请求,PHP会开启多个进程来响应对应的请求。
大型的网站往往遇到的问题就是高并发事件,比如说秒杀、抢购等电商一类,那么网站如何处理高并发问题呢?应该从哪几个方面着手考虑?今天就与大家探讨下,网站如何处理高并发。
我个人认为的一些解决方案:
1. 负载均衡,主要是以 nginx 代理服务器进行请求转发。原理:启动 nginx 有一个master主进程启动,然后一个请求(request)来了会开启一个子进程(worker),在正常的负载配置,worker进行请求平均转发到A、B、C业务服务器去处理业务,然后由业务服务器进行结果返回给nginx服务器,这里需要注意的是 worker 进行请求转发后,会停止worker而释放,存在listen程序等待业务服务器异步回调然后响应给客户端。
2.HTML界面静态化,请求网站消耗最小、相应最快的就是静态化的文件,例如社区类可以将帖子、文章一类静态化,这样可以减少数据库的请求,加载速度也会加快。
3.可以设置图片服务器分离,不管是什么服务器图片都是最消耗资源的,可以设置独立的图片服务器,这样也可以避免因为图片问题而导致系统崩溃问题。
4.在设计数据库的时候,可以再架构、扩张性上考虑,根据业务需要将数据库进行分离,例如不同的模块可以对应不同的数据库,这样数据库就具有很好的扩展性。
5.网站开发可使用缓存,各种语言都有自己的缓存模块,比如说PHP有Cache模块,Java的更多,另外还可以考虑扩展redis、memcache等,对数据库进行缓存和共享,加快响应速度。
6.使用CDN加速技术,意思就是让用户可以就近的获取到所需的内容,提高用户访问网站的响应速度,根据用户的请求地点和网络等情况,可以将用户的请求指定到离用户最近的服务器上,这也是个比较常用的方式。
针对于第 5 点,需要重点说明下,这个和我们码农程序有无bug,或者在高并发有无bug
Redis锁的正确使用https://www.jianshu.com/p/9bd3c1182dd7
public function lock($key,$expire=5,$type='ex') { if($type != 'ex' && $type != 'px') { throw new \Exception("redis lock type must be nx or ex",1); } $this->random = rand(1,4294967295); return self::$_redis->set($key, $this->random, ['nx',$type=>$expire]); // 获取锁 }
删除锁也需要注意,先对比随机数是否一致,如果一致,再删除。加上watch命令,如果在删除过程中,发现key被修改,则删除失败。避免在A进程获取key值之后,删除key之前锁过期,而这个时候B进程拿到锁(修改key值),则A删除失败。
public function unlock($key) { self::$_redis->watch($key); // 监听key if(self::$_redis->get($key) == $this->random) { // 如果是该对象的记录,则删除 self::$_redis->multi()->del($key)->exec(); return true; } else { self::$_redis->unwatch(); return true; } }
使用redis的比较完美的加锁解锁https://www.cnblogs.com/vinter/p/8626275.html
read & write 问题
这是一个经典问题,请看代码:
//redis中的某个键自增 $val = $this->redis->get($key); $val ++; $this->redis->set($val);
这段代码逻辑没有问题,就是先读取数据,再修改数据,在写回修改,这里是希望每次访问都递增变量$val的值,但在并发情况下,存在情况是两个进程都读取到了一样的初始值,然后都加1,最后写回Redis,这种情况就会统计数据比实际的少。这个问题应该有许多人遇到过,思考过怎么解决这类问题。这里给出一个统一的解决方案,就是尽量保证操作的原子性,比如可以用redis的incr命令来实现自增(可以认为redis的命令是原子的)。
加锁
由上面的问题再进一步,来探讨一个大家常用的,为一个操作进行加锁。
问题场景如下:有一个商品,每个用户都可以去修改商品信息。假设用户id分别为6和8的用户对id为123的商品进行操作。
错误示例1
$key = '123'; $val = $this->redis->get($key); if(!$val){ $this->redis->set($key,'123'); $this->redis->expire($key,'4'); /**此处修改商品信息操作 ****** **/ $this->redis->del($key); }else{ echo '错误提示'; }
上面这个错误示例,
错误点1:set和expire是分开写的,如果说程序执行中再执行了set()后出现崩溃,则这个就变成了永久锁(虽然这是个小概率事件)。
错误点2:这个商品中设置的key是商品id,val也是商品id,很多人认为只有一个key就可以了,val是什么无所谓。这就缺少了锁的标识,无法判断这个锁的拥有者是谁,从而会带来一系列影响如下。
- 用户1进程获取key对应的val,发现没有锁,所以调用了set,可能在set前,另一个用户2的进程也发现没有这个锁,也进行set,就造成了两个进程都认为自己获取到了锁的情况,
- 然后继续,如果1用户的进程执行完了操作,删除了key,用户2进程未执行完毕,此时由于无法识别是否是自己加的锁,就删除了key,这时再有新的进程进入,检查不到锁,可以立即执行,则有可能和用户2的修改冲突。
针对错误1和错误2的第1点,我们只需要去除read & write模式就可以解决,解决方案为
//同时设置val和过期时间,并使用setnx $status = $this->redis->setnx($key,$val,$expireTime); if($status){ /**此处修改商品信息操作 ****** **/ $this->redis->del($key); }else{ echo '错误提示'; }
setnx,可以在设置时检查是否存在锁不存在则设置并返回1,如果存在不覆盖并返回0。
针对错误2第2点,我们需要为每个进程设置一个独立的自己可以识别的val,如果一个用户只能开一个进程,这个val可以为用户id,如果一个用户可以设置多个进程,那么必须按照实际车情况采用其他方式来区分,这里我们以用户id为例,并且在删除的时候只能删除自己的锁。那么这里问题又出现了,如果我们写成这样:
//同时设置val和过期时间,并使用setnx $userId = 2; $status = $this->redis->setnx($key,$userId,$expireTime); if($status){ /**此处修改商品信息操作 ****** **/ if($this->redis->get($key) == $userId){ $this->redis->del($key); } }else{ echo '错误提示'; }
这种情况看似没有什么问题,其实不然,大家注意我再设置所得时候,设置了一个过期时间,假如这个时间设置的是4秒,那么如果进程A执行到删除前一刻一不小心超过了4秒,那么这个锁就自动消失了。而另一个进程B查到没有锁,就加了一把自己的锁,此时进程A执行删除,就把B的锁给删除了(极小概率事件)。
这里解决方案有两种
- 设置比较长的expire时间,弊端:设置的太长,占用内存时间长,设置的太短不能完全解决问题。(可能有人会想不设置过期时间就可以,那么回到最初的错误点,如果程序设置了锁后崩溃了就变成了永久的锁。)
- 把对比和删除弄成一个原子操作,这里呢找到了一个方法,就是用redis的eval,把语句变成原子操作。注意redis用的是lua语法,我也是新学的
//同时设置val和过期时间,并使用setnx $userId = 2; $status = $this->redis->setnx($key,$userId,$expireTime); if($status){ /**此处修改商品信息操作 ****** **/ //因为写这个博客的机器没有装redis,所以没有验证这个语法对不对。请大家见谅 $script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; $result = $this->redis->eval(script,array($key,$val),1); if ($result) { return true; } }else{ echo '错误提示'; }
这里就把两个操作变成了一个原子操作。解决的加锁和解锁可能出现的问题。
我们来说一些题外话拓展:在进程有可能出现冲突的地方,一般我们叫做临界区(操作系统中也有这个概念,是通过另一种叫做PV信号量的方式来解决的,其实可以理解为组织等待进程队列,P操作不能获取到资源使用权的则进入等待队列,等待V操作释放资源后,检查是否有等待队列,进行进程释放。当然PV操作也是原子性的。所以说解决相似问题的办法也有一定的相似性)。
Redis锁的简单应用https://www.cnblogs.com/tdws/p/5712835.html
本文版权归博客园和作者本人吴双共同所有 。转载爬虫请注明地址,博客园蜗牛 http://www.cnblogs.com/tdws/p/5712835.html
蜗牛Redis系列文章目录http://www.cnblogs.com/tdws/tag/NoSql/
Redis Cluster http://www.cnblogs.com/tdws/p/7710545.html
其实说多线程修改数据也不合适,毕竟redis服务端是单线程的,所有命令串行执行,只是在客户端并发发送命令的时候,导致串行的命令一些排列问题和网络时间差等造成数据不一致。本文虽然是数字的加减,但是为了说明锁的情况,故意不是用原子命令incr。也并非分布式锁的正确实现,没有考虑一些重入性等,稍后会整理一篇分布式锁的实践。
Redis分布式锁 http://www.cnblogs.com/tdws/p/5808528.html
ZK+curator 分布式锁 http://www.cnblogs.com/tdws/p/5874686.html
先配上一个简易的RedisHelper,一个set值,一个get值,一个设置并发锁,以便在我后面的操作中,你能清楚我究竟做了什么。
public class RedisHelper { public RedisClient client = new RedisClient("127.0.0.1", 6379); public void Set<T>(string key, T val) { client.Set(key, val); } public T Get<T>(string key) { var result = client.Get<T>(key); return result; } public IDisposable Acquire(string key) { return client.AcquireLock(key); } }
下面看一下并发代码,我只new了两个Thread。两个线程同时想访问同一个key,分别访问五万次,在并发条件下,我们很难保证数据的准确性,请比较输出结果。
static void Main(string[] args) { RedisHelper rds = new RedisHelper(); rds.Set<int>("mykey1", 0); Thread myThread1 = new Thread(AddVal); Thread myThread2 = new Thread(AddVal); myThread1.Start(); myThread2.Start(); Console.WriteLine("等待两个线程结束"); Console.ReadKey(); } public static void AddVal() { RedisHelper rds = new RedisHelper(); for (int i = 0; i < 50000; i++) { int result = rds.Get<int>("mykey1"); rds.Set<int>("mykey1", result + 1); } Console.WriteLine("线程结束,输出" + rds.Get<int>("mykey1")); }
是的,和我们单线程,跑两个50000,会输出100000。现在是两个并发线程同时跑在由于并发造成的数据结果往往不是我们想要的。那么如何解决这个问题呢,Redis已经为我们准备好了!
你可以看到我RedisHelper中有个方法是 public IDisposable Acquire(string key)。 也可以看到他返回的是IDisposable,证明我们需要手动释放资源。方法内部的 AcquireLock正是关键之处,它像redis中索取一把锁头,被锁住的资源,只能被单个线程访问,不会被两个线程同时get或者set,这两个线程一定是交替着进行的,当然这里的交替并不是指你一次我一次,也可能是你多次,我一次,下面看代码。
static void Main(string[] args) { RedisHelper rds = new RedisHelper(); rds.Set<int>("mykey1", 0); Thread myThread1 = new Thread(AddVal); Thread myThread2 = new Thread(AddVal); myThread1.Start(); myThread2.Start(); Console.WriteLine("等待两个线程结束"); Console.ReadKey(); } public static void AddVal() { RedisHelper rds = new RedisHelper(); for (int i = 0; i < 50000; i++) { using (rds.Acquire("lock")) { int result = rds.Get<int>("mykey1"); rds.Set<int>("mykey1", result + 1); } } Console.WriteLine("线程结束,输出" + rds.Get<int>("mykey1")); }
可以看到我使用了using,调用我的Acquire方法获取锁。
输出结果最后是100000,正是我们要的正确结果。前面的8W+是因为两个线程之一先执行结束了。
还有,在正式使用的过程中,建议给我们的锁,使用后删除掉,并加上一个过期时间,使用expire。
以免程序执行期间意外退出,导致锁一直存在,今后可能无法更新或者获取此被锁住的数据。
你也可以尝试一下不设置expire,在程序刚开始执行时,关闭console,重新运行程序,并且在redis-cli的操作控制台,get你锁住的值,将会永远获取不到。
所有连接此redis实例的机器,同一时刻,只能有一个获取指定name的锁.
下面是StackExchange.Redis的写法
var info = "name-"+Environment.MachineName; //如果5秒不释放锁 自动释放。避免死锁 if (db.LockTake("name", info, TimeSpan.FromSeconds(5))) { try { } catch (Exception ex) { } finally { db.LockRelease("name", token); } }