1、四层协议和七层协议的负载均衡
所谓四层就是基于IP+端口的负载均衡,通过虚拟IP+端口接收请求,然后再分配到真实的服务器;常见的四层负载均衡器有LVS和F5。
七层通过虚拟的URL或主机名接收请求,然后再分配到真实的服务器七层就是基于URL等应用层信息的负载均衡。常见的七层负载均衡器有haproxy,nginx。
2、库存大促如何在高并发的时候保证既不会多扣也不会少扣(库存超卖问题)?
有悲观锁,分布式锁,乐观锁,队列串行化,Redis原子操作几种解决方案。
1)悲观锁、队列串行化
悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。可以采用redis队列+mysql事务控制的方案。
mysql的执行代码:
beginTranse(开启事务)
try{
//quantity为请求减掉的库存数量$dbca->query('update s_store set amount = amount - quantity where postID = 12345');
$result = $dbca->query('select amount from s_store where postID = 12345');
if(result-amount <0){thrownewException('库存不足'); }
}catch($eException){
rollBack(回滚)}
commit(提交事务)
先执行update锁住本条记录,这样就能保证其他线程执行不了更新操作,可以避免超扣现象。
上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。针对这个问题我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。
2)乐观锁
乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。
mysql实现方案:数据库表增加版本字段如version,每次修改时版本号+1
如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作会把数据version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败。
PDO update更新后,不但要验证返回状态是否为true,并且同时验证影响行数是否大于0。
Redis实现方案:在 Redis 中使用 watch 命令可以决定事务是执行还是回滚。
一般而言,可以在 multi 命令之前使用 watch 命令监控某些键值对,然后使用 multi 命令开启事务,执行各类对数据结构进行操作的命令,这个时候这些命令就会进入队列。
当 Redis 使用 exec 命令执行事务的时候,它首先会去比对被 watch 命令所监控的键值对,
如果没有发生变化,那么它会执行事务队列中的命令,提交事务;
如果发生变化,那么它不会执行任何事务中的命令,而去事务回滚。
无论事务是否回滚 , Redis 都会去取消执行事务前的 watch 命令
while (true) {
System.out.println(Thread.currentThread().getName());
jedis = RedisUtil.getJedis();
try {
jedis.watch("mykey");
int stock = Integer.parseInt(jedis.get("mykey"));
if (stock > 0) {
Transaction transaction = jedis.multi();
transaction.set("mykey", String.valueOf(stock - 1));
List
if (result == null || result.isEmpty()) {
// 可能是watch-key被外部修改,或者是数据操作被驳回
System.out.println("Transaction error...");
}
} else {
System.out.println("库存为0");
break;
}
} catch (Exception e) {
e.printStackTrace();
RedisUtil.returnResource(jedis);
}finally{
RedisUtil.returnResource(jedis);
}
}
3)分布式锁
常规做法
首先我们要明确重要的一点是减库存是需要顺序的,而需要顺序就意味着不能有并发加减库存的操作,为了实现顺序,一般做法都是将多线程强行变为单线程实现同步操作或者所说的顺序,将多线程变为单线程方案普遍做法便是使用锁或者借助队列。另一方面由于大型互联网应用面向大量用户所以都是大型分布式加集群作为最基础的架构,而由于架构原因,往常所使用的lock或者Synchronized进程锁关键字失去了意义(只能锁住当前Web程序代码块,但无法锁住集群中其他Web程序)。此时我们便要借助分布式锁或者MQ组件来达到跨进程跨主机的单线程效果。
接下来我们以ABC下单减库为例说明分布式下的减库存场景
ABC同时发起库存减1的请求
服务器接收到三个减库存操作,利用分布式锁锁住了减库存的逻辑,每次只限一个请求操作.对A请求进行库存减1操作后,再对B进行操作,one by one 以此类推。
目前减库存操作运行很好,不会发生超卖情况,老板再也不担心程序员败家了。但是以上减库存的逻辑有个很大的问题便是由于强行将多线程请求变为单线程,不可避免的导致排队的发生,这样会发生什么情况呢?
越后进分布式锁或者队列的请求他需要的响应时间越久因为他的响应时间是前面所有请求的响应时间之和。当然有人会说增加配置或者在redis中减库存再利用rabbitmq将结果同步到数据库中,由于操作内存中的数据让减库存操作响应加快,这的确对单次的减库存有效,但是随着并发提高,单次减库存响应时间的优化必将遇到瓶颈。依然没有解决高并发下所有人必须强行排队导致的问题。那有没有那种又顺序执行又能相对的并行加减库存操作呢?
并行异步减库存
减库存必定是顺序排队的,这毋庸置疑,但是有没有办法可以加快这个排队呢,答案是有的!
只有将同步减库存逻辑变为异步才能从根本解决排队问题。但是有人会说这与库存操作的逻辑(同步顺序排队)冲突。
其实这里所说的异步是相对的,什么意思呢?
首先全局库存是必须顺序操作的,但是如果我们把库存分割成N块,每一块内部是顺序的,但是每一块彼此之间又是异步的。这样就很好的解决了库存顺序执行的逻辑又减轻了排队的影响。有人会问这里是如何减轻的呢?首先来给减库存算一笔响应时间的账:
假设每个减库存操作的响应时间优化到50毫秒,并发2000,按照常规做法加全局锁那第2000个人的响应时间便是前面1999个用户的响应时间加他自己的50毫秒之和为100秒。100秒的响应可能用户早就心里默默诅咒你了。而且这已经是非常理想化的单次响应时间了。如果有人说可以优化到2毫秒就不会超时了。。麻烦带上键盘去微博杠吧。。
如果使用第二种方案假设三个用户请求减库存操作,完全可以让三个请求进三个不同的锁去扣减各自的库存数,此时三人没有排队可以保证他们同时减库存,而又不影响库存总数的准确性,因为三个请求操作的是各自锁所维护的库存数。随着业务增长,库存总数的分割可以不断细分直到缩短响应时间到合理范围,而这个库存总数的分割很好的保证了不会遇到瓶颈。但是由于这种业务架构的设计,导致业务不得不变得复杂,可以看到我们在进入分布式锁之前有一个称为库存总数协调器的模块,这个模块是用来做什么的呢?
首先我们把库存分割成多块后解决的首要问题便是如何让请求均匀的依次进入每一个分布式锁中进而操作当前锁所负责的库存数。
库存协调器的逻辑完全看各位自己业务模型来决定,你可以用雪花算法均匀分布也可使用ip或者用户标识取余去覆盖到每一个锁,总之实现方式看业务情况来决定,当然了很大几率会出现有的库存块内的库存总数消耗完了但有的还剩余,所以库存协调器一定要考虑到这类情况及时将库存较多的库存块内的库存数分散给其他库存块,以达到多线程减库存的效果。
从示例图中可以看到引入了rabbitmq,他在当前整个业务架构中的作用主要是每一个分布式锁处理完当前库存块的库存后要将当前加减的数量丢给消息队列,由消费端慢慢消化这些操作到数据库。
● 其实这就是分段加锁。你想,假如你现在iphone有1000个库存,那么你完全可以给拆成20个库存段,要是你愿意,可以在数据库的表里建20个库存字段,比如stock_01,stock_02,类似这样的,也可以在redis之类的地方放20个库存key。
● 总之,就是把你的1000件库存给他拆开,每个库存段是50件库存,比如stock_01对应50件库存,stock_02对应50件库存。
● 接着,每秒1000个请求过来了,好!此时其实可以是自己写一个简单的随机算法,每个请求都是随机在20个分段库存里,选择一个进行加锁。
● bingo!这样就好了,同时可以有最多20个下单请求一起执行,每个下单请求锁了一个库存分段,然后在业务逻辑里面,就对数据库或者是Redis中的那个分段库存进行操作即可,包括查库存 -> 判断库存是否充足 -> 扣减库存。
● 这相当于什么呢?相当于一个20毫秒,可以并发处理掉20个下单请求,那么1秒,也就可以依次处理掉20 * 50 = 1000个对iphone的下单请求了。
● 一旦对某个数据做了分段处理之后,有一个坑大家一定要注意:就是如果某个下单请求,咔嚓加锁,然后发现这个分段库存里的库存不足了,此时咋办?
● 这时你得自动释放锁,然后立马换下一个分段库存,再次尝试加锁后尝试处理。这个过程一定要实现。
参考:https://blog.csdn.net/sD7O95O/article/details/101398241
https://blog.csdn.net/u010391342/article/details/84372342
4)Redis原子操作
incr/decr原子性操作,incr增加,decr减少
//此处不可以取出后放入php变量判断库存,否则会出现幻读,导致超卖
if ($redis_client->decrby($key, $parmas['num']) > -1) { //利用原子操作将库存数量减去需要的商品数量,如果返回值大于-1则说明库存量满足要求
//减库存
$goods = new Goods();
$parmas['version'] = 1;
return $goods->subInventory($parmas);
} else {
//购买多个时,如库存不足,需要把数量加回去,否则会出现库减库存,商品并没有卖出去
$redis_client->incrby($key, $parmas['num']);
return false;
}
使用redis测试时,每次修改完库存需要删除KEY:DEL goods_1