这一次总结和分享用Redis实现分布式锁来完成电商的秒杀功能。先扯点个人观点,之前我看了一篇博文说博客园的文章大部分都是分享代码,博文里强调说分享思路比分享代码更重要(貌似大概是这个意思,若有误请谅解),但我觉得,分享思路固然重要,但有了思路,却没有实现的代码,那会让人觉得很浮夸的,在工作中的程序猿都知道,你去实现一个功能模块,一段代码,虽然你有了思路,但是实现的过程也是很耗时的,特别是代码调试,还有各种测试等等。所以我认为,思路+代码,才是一篇好博文的主要核心。

  直接进入主题。

  一、前言

  双十一刚过不久,大家都知道在天猫、京东、苏宁等等电商网站上有很多秒杀活动,例如在某一个时刻抢购一个原价1999现在秒杀价只要999的手机时,会迎来一个用户请求的高峰期,可能会有几十万几百万的并发量,来抢这个手机,在高并发的情形下会对数据库服务器或者是文件服务器应用服务器造成巨大的压力,严重时说不定就宕机了,另一个问题是,秒杀的东西都是有量的,例如一款手机只有10台的量秒杀,那么,在高并发的情况下,成千上万条数据更新数据库(例如10台的量被人抢一台就会在数据集某些记录下 减1),那次这个时候的先后顺序是很乱的,很容易出现10台的量,抢到的人就不止10个这种严重的问题。那么,以后所说的问题我们该如何去解决呢? 接下来我所分享的技术就可以拿来处理以上的问题: 分布式锁。

  二、实现思路

  1.Redis实现分布式锁思路

    思路很简单,主要用到的redis函数是setnx(),这个应该是实现分布式锁最主要的函数。首先是将某一任务标识名(这里用Lock:order作为标识名的例子)作为键存到redis里,并为其设个过期时间,如果是还有Lock:order请求过来,先是通过setnx()看看是否能将Lock:order插入到redis里,可以的话就返回true,不可以就返回false。当然,在我的代码里会比这个思路复杂一些,我会在分析代码时进一步说明。

  2.Redis实现任务队列

    这里的实现会用到上面的Redis分布式的锁机制,主要是用到了Redis里的有序集合这一数据结构。例如入队时,通过zset的add()函数进行入队,而出队时,可以用到zset的getScore()函数。另外还可以弹出顶部的几个任务。

  以上就是实现 分布式锁 和 任务队列 的简单思路,如果你看完有点模棱两可,那请看接下来的代码实现。

  三、代码分析

  (一)先来分析Redis分布式锁的代码实现  

    (1)为避免特殊原因导致锁无法释放,在加锁成功后,锁会被赋予一个生存时间(通过lock方法的参数设置或者使用默认值),超出生存时间锁会被自动释放锁的生存时间默认比较短(秒级),因此,若需要长时间加锁,可以通过expire方法延长锁的生存时间为适当时间,比如在循环内。

    (2)系统级的锁当进程无论何种原因时出现crash时,操作系统会自己回收锁,所以不会出现资源丢失,但分布式锁不用,若一次性设置很长时间,一旦由于各种原因出现进程crash 或者其他异常导致unlock未被调用时,则该锁在剩下的时间就会变成垃圾锁,导致其他进程或者进程重启后无法进入加锁区域。

    先看加锁的实现代码:这里需要主要两个参数,一个是$timeout,这个是循环获取锁的等待时间,在这个时间内会一直尝试获取锁知道超时,如果为0,则表示获取锁失败后直接返回而不再等待;另一个重要参数的$expire,这个参数指当前锁的最大生存时间,以秒为单位的,它必须大于0,如果超过生存时间锁仍未被释放,则系统会自动强制释放。这个参数的最要作用请看上面的(1)里的解释。

    这里先取得当前时间,然后再获取到锁失败时的等待超时的时刻(是个时间戳),再获取到锁的最大生存时刻是多少。这里redis的key用这种格式:"Lock:锁的标识名",这里就开始进入循环了,先是插入数据到redis里,使用setnx()函数,这函数的意思是,如果该键不存在则插入数据,将最大生存时刻作为值存储,假如插入成功,则对该键进行失效时间的设置,并将该键放在$lockedName数组里,返回true,也就是上锁成功;如果该键存在,则不会插入操作了,这里有一步严谨的操作,那就是取得当前键的剩余时间,假如这个时间小于0,表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建)如果出现这种状况,那就是进程的某个实例setnx成功后 crash 导致紧跟着的expire没有被调用,这时可以直接设置expire并把锁纳为己用。如果没设置锁失败的等待时间 或者 已超过最大等待时间了,那就退出循环,反之则 隔 $waitIntervalUs 后继续 请求。 这就是加锁的整一个代码分析。

/**
* 加锁
* @param [type] $name 锁的标识名
* @param integer $timeout 循环获取锁的等待超时时间,在此时间内会一直尝试获取锁直到超时,为0表示失败后直接返回不等待
* @param integer $expire 当前锁的最大生存时间(秒),必须大于0,如果超过生存时间锁仍未被释放,则系统会自动强制释放
* @param integer $waitIntervalUs 获取锁失败后挂起再试的时间间隔(微秒)
* @return [type] [description]
*/
public function lock($name, $timeout = 0, $expire = 15, $waitIntervalUs = 100000) {
if ($name == null) return false; //取得当前时间
$now = time();
//获取锁失败时的等待超时时刻
$timeoutAt = $now + $timeout;
//锁的最大生存时刻
$expireAt = $now + $expire;
$redisKey = "Lock:{$name}";
while (true) {
//将rediskey的最大生存时刻存到redis里,过了这个时刻该锁会被自动释放
$result = $this->redisString->setnx($redisKey, $expireAt);
if ($result != false) {
//设置key的失效时间
$this->redisString->expire($redisKey, $expireAt);
//将锁标志放到lockedNames数组里
$this->lockedNames[$name] = $expireAt;
return true;
}
//以秒为单位,返回给定key的剩余生存时间
$ttl = $this->redisString->ttl($redisKey);
//ttl小于0 表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建)
//如果出现这种状况,那就是进程的某个实例setnx成功后 crash 导致紧跟着的expire没有被调用
//这时可以直接设置expire并把锁纳为己用
if ($ttl < 0) {
$this->redisString->set($redisKey, $expireAt);
$this->lockedNames[$name] = $expireAt;
return true;
}
/*****循环请求锁部分*****/
//如果没设置锁失败的等待时间 或者 已超过最大等待时间了,那就退出
if ($timeout <= 0 || $timeoutAt < microtime(true)) break;
//隔 $waitIntervalUs 后继续 请求
usleep($waitIntervalUs);
}
return false;
}

接着看解锁的代码分析:解锁就简单多了,传入参数就是锁标识,先是判断是否存在该锁,存在的话,就从redis里面通过deleteKey()函数删除掉锁标识即可。

/**
* 解锁
* @param [type] $name [description]
* @return [type] [description]
*/
public function unlock($name) {
//先判断是否存在此锁
if ($this->isLocking($name)) {
//删除锁
if ($this->redisString->deleteKey("Lock:$name")) {
//清掉lockedNames里的锁标志
unset($this->lockedNames[$name]);
return true;
}
}
return false;
}

再贴上删除掉所有锁的方法,其实都一个样,多了个循环遍历而已。

/**
* 释放当前所有获得的锁
* @return [type] [description]
*/
public function unlockAll() {
//此标志是用来标志是否释放所有锁成功
$allSuccess = true;
foreach ($this->lockedNames as $name => $expireAt) {
if (false === $this->unlock($name)) {
$allSuccess = false;
}
}
return $allSuccess;
}

以上就是用Redis实现分布式锁的整一套思路和代码实现的总结和分享,这里我附上正一个实现类的代码,代码里我基本上对每一行进行了注释,方便大家快速看懂并且能模拟应用。想要深入了解的请看整个类的代码:

/**
*在redis上实现分布式锁
*/
class RedisLock {
private $redisString;
private $lockedNames = [];
public function __construct($param = NULL) {
$this->redisString = RedisFactory::get($param)->string;
} /**
* 加锁
* @param [type] $name 锁的标识名
* @param integer $timeout 循环获取锁的等待超时时间,在此时间内会一直尝试获取锁直到超时,为0表示失败后直接返回不等待
* @param integer $expire 当前锁的最大生存时间(秒),必须大于0,如果超过生存时间锁仍未被释放,则系统会自动强制释放
* @param integer $waitIntervalUs 获取锁失败后挂起再试的时间间隔(微秒)
* @return [type] [description]
*/
public function lock($name, $timeout = 0, $expire = 15, $waitIntervalUs = 100000) {
if ($name == null) return false; //取得当前时间
$now = time();
//获取锁失败时的等待超时时刻
$timeoutAt = $now + $timeout;
//锁的最大生存时刻
$expireAt = $now + $expire; $redisKey = "Lock:{$name}";
while (true) {
//将rediskey的最大生存时刻存到redis里,过了这个时刻该锁会被自动释放
$result = $this->redisString->setnx($redisKey, $expireAt); if ($result != false) {
//设置key的失效时间
$this->redisString->expire($redisKey, $expireAt);
//将锁标志放到lockedNames数组里
$this->lockedNames[$name] = $expireAt;
return true;
} //以秒为单位,返回给定key的剩余生存时间
$ttl = $this->redisString->ttl($redisKey); //ttl小于0 表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建)
//如果出现这种状况,那就是进程的某个实例setnx成功后 crash 导致紧跟着的expire没有被调用
//这时可以直接设置expire并把锁纳为己用
if ($ttl < 0) {
$this->redisString->set($redisKey, $expireAt);
$this->lockedNames[$name] = $expireAt;
return true;
} /*****循环请求锁部分*****/
//如果没设置锁失败的等待时间 或者 已超过最大等待时间了,那就退出
if ($timeout <= 0 || $timeoutAt < microtime(true)) break; //隔 $waitIntervalUs 后继续 请求
usleep($waitIntervalUs); } return false;
}
/**
* 解锁
* @param [type] $name [description]
* @return [type] [description]
*/
public function unlock($name) {
//先判断是否存在此锁
if ($this->isLocking($name)) {
//删除锁
if ($this->redisString->deleteKey("Lock:$name")) {
//清掉lockedNames里的锁标志
unset($this->lockedNames[$name]);
return true;
}
}
return false;
}
/**
* 释放当前所有获得的锁
* @return [type] [description]
*/
public function unlockAll() {
//此标志是用来标志是否释放所有锁成功
$allSuccess = true;
foreach ($this->lockedNames as $name => $expireAt) {
if (false === $this->unlock($name)) {
$allSuccess = false;
}
}
return $allSuccess;
}
/**
* 给当前所增加指定生存时间,必须大于0
* @param [type] $name [description]
* @return [type] [description]
*/
public function expire($name, $expire) {
//先判断是否存在该锁
if ($this->isLocking($name)) {
//所指定的生存时间必须大于0
$expire = max($expire, 1);
//增加锁生存时间
if ($this->redisString->expire("Lock:$name", $expire)) {
return true;
}
}
return false;
}
/**
* 判断当前是否拥有指定名字的所
* @param [type] $name [description]
* @return boolean [description]
*/
public function isLocking($name) {
//先看lonkedName[$name]是否存在该锁标志名
if (isset($this->lockedNames[$name])) {
//从redis返回该锁的生存时间
return (string)$this->lockedNames[$name] = (string)$this->redisString->get("Lock:$name");
} return false;
}
}

  

php实现商城秒杀的更多相关文章

  1. php商城秒杀活动

    今天在网上看到一篇思路+代码的商城秒杀实例,我觉得非常不错,借鉴一下分享给大家: 一.前言 双十一刚过不久,大家都知道在天猫.京东.苏宁等等电商网站上有很多秒杀活动,例如在某一个时刻抢购一个原价199 ...

  2. Java商城秒杀系统的设计与实战视频教程(SpringBoot版)_汇总贴

    51CTO学院 Java商城秒杀系统的设计与实战视频教程(SpringBoot版) H:\BaiDu\微服务0830\2019最新 Java商城秒杀系统的设计与实战视频教程(SpringBoot版) ...

  3. Java商城秒杀系统的设计与实战视频教程(SpringBoot版)

    课程目标掌握如何基于Spring Boot构建秒杀系统或者高并发业务系统,以及构建系统时采用的前后端技术栈适用人群Spring Boot实战者,微服务或分布式系统架构实战者,秒杀系统和高并发实战者,中 ...

  4. JAVA构建高并发商城秒杀系统——架构分析

    面试场景 我们打算组织一个并发一万人的秒杀活动,1元秒杀100个二手元牙刷,你给我说说解决方案. 秒杀/抢购业务场景 商品秒杀.商品抢购.群红包.抢优惠劵.抽奖....... 秒杀/抢购业务特点 秒杀 ...

  5. 商城秒杀系统总结(Java)

    本文写的较为零散,对没有基础的同学不太友好. 一.秒杀系统项目总结(基础版) classpath 在.properties中时常需要读取资源,定位文件地址时经常用到classpath 类路径指的是sr ...

  6. PHP高并发商城秒杀

    1.什么是秒杀 秒杀活动是一些购物平台推出的集中人气的活动,一般商品数量很少,价格很便宜,限定开始购买的时间,会在以秒为单位的时间内被购买一空.比如原价千元甚至万元的商品以一元的价格出售,但数量只有一 ...

  7. 2019最新 Java商城秒杀系统的设计与实战视频教程(SpringBoot版)_2-2微服务项目的搭建-SpringBoot搭建多模块项目二

    一些重要的配置文件直接复制过来了 jdbc和shiro的配置 application.properties里面的相关配置项的含义 日志界别的配置 数据返回到前端的json的配置 数据源的配置 需要新建 ...

  8. thinkphp+redis实现秒杀功能

    好久没来整理文章了,闲了没事写篇文章记录下php+redis实现商城秒杀功能. 1,安装redis,根据自己的php版本安装对应的redis扩展(此步骤简单的描述一下) 1.1,安装 php_igbi ...

  9. Java秒杀系统实战系列~整体业务流程介绍与数据库设计

    摘要: 本篇博文是“Java秒杀系统实战系列文章”的第三篇,本篇博文将主要介绍秒杀系统的整体业务流程,并根据相应的业务流程进行数据库设计,最终采用Mybatis逆向工程生成相应的实体类Entity.操 ...

随机推荐

  1. git bash下添加忽略文件列表

    转载自:https://blog.csdn.net/weixin_42808389/article/details/81232119 在用KEIL 5(MDK ARM)开发项目时需要用到GIT管理代码 ...

  2. 【Offer】[49] 【丑数】

    题目描述 思路分析 测试用例 Java代码 代码链接 题目描述 我们把只包含因子2.3和5的数称作丑数( Ugly Number).求按从小到大的顺序的第1500个丑数.例如,6.8都是丑数,但14不 ...

  3. Python学习之turtle库和蟒蛇绘制程序

    Python的函数库 Python语言与C语言Java类似,可以大量使用外部函数库包含在安装包中的函数库:. 比如math, random, turtle等其他函数库,其他函数库用户根据代码需求自行安 ...

  4. Django之模型层(2)

    Django之模型层(2) 一.创建模型 实例:我们来假定下面这些概念,字段和关系. 作者模型:一个作者由姓名和年龄. 作者详细模型:把作者的详情放到详情表,包含生日,手机号,家庭住址等信息.作者详情 ...

  5. Springboot国际化信息(i18n)解析

    国际化信息理解 国际化信息也称为本地化信息 . Java 通过 java.util.Locale 类来表示本地化对象,它通过 “语言类型” 和 “国家/地区” 来创建一个确定的本地化对象 .举个例子吧 ...

  6. Linux下Mysql启动异常排查方案

    遇到Mysql启动异常问题,可以从以下几个方面依次进行问题排查: (1)如果遇到“Can't connect to local MySQL server through socket '/tmp/my ...

  7. cssrelative

    <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...

  8. 《程序实现》从xml、txt文件里读取数据写入excel表格

    直接上码 import java.io.BufferedReader; import java.io.DataInputStream; import java.io.File; import java ...

  9. apache ignite系列(四):持久化

    ignite持久化与固化内存 1.持久化的机制 ignite持久化的关键点如下: ignite持久化可防止内存溢出导致数据丢失的情况: 持久化可以定制化配置,按需持久化; 持久化能解决在大量缓存数据情 ...

  10. Linux 笔记 - 第十四章 LAMP 之(一) 环境搭建

    博客地址:http://www.moonxy.com 一.前言 LAMP 是 Linux Apache MySQL PHP 的简写,即把 Apache.MySQL 以及 PHP 安装在 Linux 系 ...