java 用redis如何处理电商平台,秒杀、抢购超卖
2017-04-28 17:12
453 查看
看到这片文章不错,分享出来与大家共勉
一、刚来公司时间不长,看到公司原来的同事写了这样一段代码,下面贴出来:
1、这是在一个方法调用下面代码的部分:
[java] view
plain copy
if (!this.checkSoldCountByRedisDate(key, limitCount, buyCount, endDate)) {// 标注10:
throw new ServiceException("您购买的商品【" + commodityTitle + "】,数量已达到活动限购量");
}
2、下面是判断超卖的方法:
[java] view
plain copy
/** 根据缓存数据查询是否卖超 */
//标注:1;synchronized
private synchronized boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate) {
boolean flag = false;
if (redisUtil.exists(key)) {//标注:2;redisUtil.exists(key)
Integer soldCount = (int) redisUtil.get(key);//标注:3;redisUtil.get(key)
Integer totalSoldCount = soldCount + buyCount;
if (limitCount > (totalSoldCount)) {
flag = false;//标注:4;flag = false
} else {
if (redisUtil.tryLock(key, 80)) {//标注:5;rdisUtil.tryLock(key, 80)
redisUtil.remove(key);// 解锁 //标注:6;redisUtil.remove(key)
redisUtil.set(key, totalSoldCount);//标注:7;redisUtil.set(key, totalSoldCount)
flag = true;
} else {
throw new ServiceException("活动太火爆啦,请稍后重试");
}
}
} else {
//标注:8;redisUtil.set(key, new String("buyCount"), DateUtil.diffDateTime(endDate, new Date()))
redisUtil.set(key, new String("buyCount"), DateUtil.diffDateTime(endDate, new Date()));
flag = false;
}
return flag;
}
3、上面提到的redisUtil类中的方法,其中redisTemplate为org.springframework.data.redis.core.RedisTemplate;这个不了解的可以去网上找下,spring-data-redis.jar的相关文档,贴出来redisUtil用到的相关方法:
[java] view
plain copy
/**
* 判断缓存中是否有对应的value
*
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 将键值对设定一个指定的时间timeout.
*
* @param key
* @param timeout
* 键值对缓存的时间,单位是毫秒
* @return 设置成功返回true,否则返回false
*/
public boolean tryLock(String key, long timeout) {
boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(key, "");
if (isSuccess) {//标注:9;redisTemplate.expire
redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS);
}
return isSuccess;
}
/**
* 读取缓存
*
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 删除对应的value
*
* @param key
*/
public void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 写入缓存
*
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
return set(key, value, null);
}
/**
*
* @Title: set
* @Description: 写入缓存带有效期
* @param key
* @param value
* @param expireTime
* @return boolean 返回类型
* @throws
*/
public boolean set(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
if (expireTime != null) {
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
}
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
4、上面提到的DateUtil类,我会在下面用文件的形式发出来!
二、现在我们来解读下这段代码,看看作者的意图,以及问题点在什么地方,这样帮助更多的人了解,在电商平台如何处理在抢购、秒杀时出现的超卖的情况处理
1、参数说明,上面checkSoldCountByRedisDate方法,有4个参数分别是:
key:购买数量的计数,放于Redis缓存中的key;
limitCount:查找源码发现,原注释为:总限购数量;
buyCount:为当前一次请求下单要购买的数量;
endDate:活动结束时间;
2、通过上面的标注,我们来解析原作者的意图:
标注1:想通过synchronized关键字实现同步,看似没问题
标注2:通过redisUtil.exists方法判断key是否存在,看似没什么问题
标注3:redisUtil.get(key)获取购买总数,似乎也没问题
标注4:当用户总购买数量<总限购量返回false,看起来只是一个简单的判断
标注5:想通过redisUtil.tryLock加锁,实现超卖的处理,后面的代码实现计数,好像也没什么问题
标注6:标注5加了锁,那么通过redisUtil.remove解锁,看起来顺理成章
标注7:通过redisUtil.set来记录用户购买量,原作者应该是这个意思了
标注8:如果标注2判断的key不存在,在这里创建一个key,看起来代码好像也是要这么写
标注9:我想原作者是不想出现死锁,用redisTemplate.expire做锁超时的方式来解除死锁,这样是可以的
3、针对上面作者意图的分析,我们来看下,看似没有问题的,是否真的就是没问题!呵呵。。,好贱!
下面看看每个标注,可能会出现的问题:
标注1:synchronized关键字,在分布式高并发的情况下,不能实现同步处理,不信测试下就知道了;
那么就可能会出现 的问题是:
现在同一用户发起请A、B或不同用户发起请求A、B,会同时进入checkSoldCountByRedisDate方法并执行
标注2:当抢购开始时,A、B请求同时率先抢购,进入checkSoldCountByRedisDate方法,
A、B请求被redisUtil.exists方法判断key不存在,
从而执行了标注8的部分,同时去执行一个创建key的动作;
真的是好坑啊!第一个开始抢购都抢不到!
标注3:当请求A、B同时到达时,假设:请求A、B当前购买buyCount参数为40,标注3得到的soldCount=50,limitCount=100,
此时请求A、B得到的totalSoldCount均为90,问题又来了
标注4:limitCount > (totalSoldCount):totalSoldCount=90,limitCount=100,些时flag就等于 false,
返回给标注10的位置抛出异常信息(throw new ServiceException("您购买的商品【" + commodityTitle + "】,数量已达到活动限购量"););
请求A、B都没抢到商品。什么鬼?总共购买90,总限购量是100,这就抛出异常达到活动限购数,我开始看不懂了
标注5:在这里加锁的时候,如果当执行到标注9:isSuccess=true,客户端中断,不执行标注9以后的代码,
完蛋,死锁出现了!谁都别想抢到
下面我们假设A请求比B请求稍慢一点儿到达时,A、B请求的buyCount参数为40,标注3得到的soldCount=50、limitCount=100去执行的else里面的代码,
也就checkSoldCountByRedisDate方法中的:
[java] view
plain copy
else {
if (redisUtil.tryLock(key, 80)) {
redisUtil.remove(key);// 解锁
redisUtil.set(key, totalSoldCount);
flag = true;
} else {
throw new ServiceException("活动太火爆啦,请稍后重试");
}
}
标注6、7:A请求先到达,假设加锁成功,并成功释放锁,设置的key的值为90后,这里B请求也加锁成功,释放锁成功,设置key的值为90,
那么问题来了:
A、B各买40,原购买数为50,总限量数为100,40+40+50=130,大于最大限量数却成功执行,我了个去,公司怎么向客户交代!
凌晨了,废话不多说了,关键还要看问题怎么处理,直接上代码吧!调用的地方就不看了,其实,代码也没几行,有注释大家一看就明白了:
[java] view
plain copy
/**
*
* 雷------2016年6月17日
*
* @Title: checkSoldCountByRedisDate
* @Description: 抢购的计数处理(用于处理超卖)
* @param @param key 购买计数的key
* @param @param limitCount 总的限购数量
* @param @param buyCount 当前购买数量
* @param @param endDate 抢购结束时间
* @param @param lock 锁的名称与unDieLock方法的lock相同
* @param @param expire 锁占有的时长(毫秒)
* @param @return 设定文件
* @return boolean 返回类型
* @throws
*/
private boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate, String lock, int expire) {
boolean check = false;
if (this.lock(lock, expire)) {
Integer soldCount = (Integer) redisUtil.get(key);
Integer totalSoldCount = (soldCount == null ? 0 : soldCount) + buyCount;
if (totalSoldCount <= limitCount) {
redisUtil.set(key, totalSoldCount, DateUtil.diffDateTime(endDate, new Date()));
check = true;
}
redisUtil.remove(lock);
} else {
if (this.unDieLock(lock)) {
logger.info("解决了出现的死锁");
} else {
throw new ServiceException("活动太火爆啦,请稍后重试");
}
}
return check;
}
/**
*
* 雷------2016年6月17日
*
* @Title: lock
* @Description: 加锁机制
* @param @param lock 锁的名称
* @param @param expire 锁占有的时长(毫秒)
* @param @return 设定文件
* @return Boolean 返回类型
* @throws
*/
@SuppressWarnings("unchecked")
public Boolean lock(final String lock, final int expire) {
return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
boolean locked = false;
byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtil.getDateAddMillSecond(null, expire));
byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);
locked = connection.setNX(lockName, lockValue);
if (locked)
connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.MILLISECONDS));
return locked;
}
});
}
/**
*
* 雷------2016年6月17日
*
* @Title: unDieLock
* @Description: 处理发生的死锁
* @param @param lock 是锁的名称
* @param @return 设定文件
* @return Boolean 返回类型
* @throws
*/
@SuppressWarnings("unchecked")
public Boolean unDieLock(final String lock) {
boolean unLock = false;
Date lockValue = (Date) redisTemplate.opsForValue().get(lock);
if (lockValue != null && lockValue.getTime() <= (new Date().getTime())) {
redisTemplate.delete(lock);
unLock = true;
}
return unLock;
}
下面会把上面方法中用到的相关DateUtil类的方法贴出来:
[java] view
plain copy
/**
* 日期相减(返回秒值)
* @param date Date
* @param date1 Date
* @return int
* @author
*/
public static Long diffDateTime(Date date, Date date1) {
return (Long) ((getMillis(date) - getMillis(date1))/1000);
}
public static long getMillis(Date date) {
Calendar c = Calendar.getInstance();
c.setTime(date);
return c.getTimeInMillis();
}
/**
* 获取 指定日期 后 指定毫秒后的 Date
*
* @param date
* @param millSecond
* @return
*/
public static Date getDateAddMillSecond(Date date, int millSecond) {
Calendar cal = Calendar.getInstance();
if (null != date) {// 没有 就取当前时间
cal.setTime(date);
}
cal.add(Calendar.MILLISECOND, millSecond);
return cal.getTime();
}
到这里就结束!
一、刚来公司时间不长,看到公司原来的同事写了这样一段代码,下面贴出来:
1、这是在一个方法调用下面代码的部分:
[java] view
plain copy
if (!this.checkSoldCountByRedisDate(key, limitCount, buyCount, endDate)) {// 标注10:
throw new ServiceException("您购买的商品【" + commodityTitle + "】,数量已达到活动限购量");
}
2、下面是判断超卖的方法:
[java] view
plain copy
/** 根据缓存数据查询是否卖超 */
//标注:1;synchronized
private synchronized boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate) {
boolean flag = false;
if (redisUtil.exists(key)) {//标注:2;redisUtil.exists(key)
Integer soldCount = (int) redisUtil.get(key);//标注:3;redisUtil.get(key)
Integer totalSoldCount = soldCount + buyCount;
if (limitCount > (totalSoldCount)) {
flag = false;//标注:4;flag = false
} else {
if (redisUtil.tryLock(key, 80)) {//标注:5;rdisUtil.tryLock(key, 80)
redisUtil.remove(key);// 解锁 //标注:6;redisUtil.remove(key)
redisUtil.set(key, totalSoldCount);//标注:7;redisUtil.set(key, totalSoldCount)
flag = true;
} else {
throw new ServiceException("活动太火爆啦,请稍后重试");
}
}
} else {
//标注:8;redisUtil.set(key, new String("buyCount"), DateUtil.diffDateTime(endDate, new Date()))
redisUtil.set(key, new String("buyCount"), DateUtil.diffDateTime(endDate, new Date()));
flag = false;
}
return flag;
}
3、上面提到的redisUtil类中的方法,其中redisTemplate为org.springframework.data.redis.core.RedisTemplate;这个不了解的可以去网上找下,spring-data-redis.jar的相关文档,贴出来redisUtil用到的相关方法:
[java] view
plain copy
/**
* 判断缓存中是否有对应的value
*
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 将键值对设定一个指定的时间timeout.
*
* @param key
* @param timeout
* 键值对缓存的时间,单位是毫秒
* @return 设置成功返回true,否则返回false
*/
public boolean tryLock(String key, long timeout) {
boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(key, "");
if (isSuccess) {//标注:9;redisTemplate.expire
redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS);
}
return isSuccess;
}
/**
* 读取缓存
*
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 删除对应的value
*
* @param key
*/
public void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 写入缓存
*
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
return set(key, value, null);
}
/**
*
* @Title: set
* @Description: 写入缓存带有效期
* @param key
* @param value
* @param expireTime
* @return boolean 返回类型
* @throws
*/
public boolean set(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
if (expireTime != null) {
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
}
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
4、上面提到的DateUtil类,我会在下面用文件的形式发出来!
二、现在我们来解读下这段代码,看看作者的意图,以及问题点在什么地方,这样帮助更多的人了解,在电商平台如何处理在抢购、秒杀时出现的超卖的情况处理
1、参数说明,上面checkSoldCountByRedisDate方法,有4个参数分别是:
key:购买数量的计数,放于Redis缓存中的key;
limitCount:查找源码发现,原注释为:总限购数量;
buyCount:为当前一次请求下单要购买的数量;
endDate:活动结束时间;
2、通过上面的标注,我们来解析原作者的意图:
标注1:想通过synchronized关键字实现同步,看似没问题
标注2:通过redisUtil.exists方法判断key是否存在,看似没什么问题
标注3:redisUtil.get(key)获取购买总数,似乎也没问题
标注4:当用户总购买数量<总限购量返回false,看起来只是一个简单的判断
标注5:想通过redisUtil.tryLock加锁,实现超卖的处理,后面的代码实现计数,好像也没什么问题
标注6:标注5加了锁,那么通过redisUtil.remove解锁,看起来顺理成章
标注7:通过redisUtil.set来记录用户购买量,原作者应该是这个意思了
标注8:如果标注2判断的key不存在,在这里创建一个key,看起来代码好像也是要这么写
标注9:我想原作者是不想出现死锁,用redisTemplate.expire做锁超时的方式来解除死锁,这样是可以的
3、针对上面作者意图的分析,我们来看下,看似没有问题的,是否真的就是没问题!呵呵。。,好贱!
下面看看每个标注,可能会出现的问题:
标注1:synchronized关键字,在分布式高并发的情况下,不能实现同步处理,不信测试下就知道了;
那么就可能会出现 的问题是:
现在同一用户发起请A、B或不同用户发起请求A、B,会同时进入checkSoldCountByRedisDate方法并执行
标注2:当抢购开始时,A、B请求同时率先抢购,进入checkSoldCountByRedisDate方法,
A、B请求被redisUtil.exists方法判断key不存在,
从而执行了标注8的部分,同时去执行一个创建key的动作;
真的是好坑啊!第一个开始抢购都抢不到!
标注3:当请求A、B同时到达时,假设:请求A、B当前购买buyCount参数为40,标注3得到的soldCount=50,limitCount=100,
此时请求A、B得到的totalSoldCount均为90,问题又来了
标注4:limitCount > (totalSoldCount):totalSoldCount=90,limitCount=100,些时flag就等于 false,
返回给标注10的位置抛出异常信息(throw new ServiceException("您购买的商品【" + commodityTitle + "】,数量已达到活动限购量"););
请求A、B都没抢到商品。什么鬼?总共购买90,总限购量是100,这就抛出异常达到活动限购数,我开始看不懂了
标注5:在这里加锁的时候,如果当执行到标注9:isSuccess=true,客户端中断,不执行标注9以后的代码,
完蛋,死锁出现了!谁都别想抢到
下面我们假设A请求比B请求稍慢一点儿到达时,A、B请求的buyCount参数为40,标注3得到的soldCount=50、limitCount=100去执行的else里面的代码,
也就checkSoldCountByRedisDate方法中的:
[java] view
plain copy
else {
if (redisUtil.tryLock(key, 80)) {
redisUtil.remove(key);// 解锁
redisUtil.set(key, totalSoldCount);
flag = true;
} else {
throw new ServiceException("活动太火爆啦,请稍后重试");
}
}
标注6、7:A请求先到达,假设加锁成功,并成功释放锁,设置的key的值为90后,这里B请求也加锁成功,释放锁成功,设置key的值为90,
那么问题来了:
A、B各买40,原购买数为50,总限量数为100,40+40+50=130,大于最大限量数却成功执行,我了个去,公司怎么向客户交代!
凌晨了,废话不多说了,关键还要看问题怎么处理,直接上代码吧!调用的地方就不看了,其实,代码也没几行,有注释大家一看就明白了:
[java] view
plain copy
/**
*
* 雷------2016年6月17日
*
* @Title: checkSoldCountByRedisDate
* @Description: 抢购的计数处理(用于处理超卖)
* @param @param key 购买计数的key
* @param @param limitCount 总的限购数量
* @param @param buyCount 当前购买数量
* @param @param endDate 抢购结束时间
* @param @param lock 锁的名称与unDieLock方法的lock相同
* @param @param expire 锁占有的时长(毫秒)
* @param @return 设定文件
* @return boolean 返回类型
* @throws
*/
private boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate, String lock, int expire) {
boolean check = false;
if (this.lock(lock, expire)) {
Integer soldCount = (Integer) redisUtil.get(key);
Integer totalSoldCount = (soldCount == null ? 0 : soldCount) + buyCount;
if (totalSoldCount <= limitCount) {
redisUtil.set(key, totalSoldCount, DateUtil.diffDateTime(endDate, new Date()));
check = true;
}
redisUtil.remove(lock);
} else {
if (this.unDieLock(lock)) {
logger.info("解决了出现的死锁");
} else {
throw new ServiceException("活动太火爆啦,请稍后重试");
}
}
return check;
}
/**
*
* 雷------2016年6月17日
*
* @Title: lock
* @Description: 加锁机制
* @param @param lock 锁的名称
* @param @param expire 锁占有的时长(毫秒)
* @param @return 设定文件
* @return Boolean 返回类型
* @throws
*/
@SuppressWarnings("unchecked")
public Boolean lock(final String lock, final int expire) {
return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
boolean locked = false;
byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtil.getDateAddMillSecond(null, expire));
byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);
locked = connection.setNX(lockName, lockValue);
if (locked)
connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.MILLISECONDS));
return locked;
}
});
}
/**
*
* 雷------2016年6月17日
*
* @Title: unDieLock
* @Description: 处理发生的死锁
* @param @param lock 是锁的名称
* @param @return 设定文件
* @return Boolean 返回类型
* @throws
*/
@SuppressWarnings("unchecked")
public Boolean unDieLock(final String lock) {
boolean unLock = false;
Date lockValue = (Date) redisTemplate.opsForValue().get(lock);
if (lockValue != null && lockValue.getTime() <= (new Date().getTime())) {
redisTemplate.delete(lock);
unLock = true;
}
return unLock;
}
下面会把上面方法中用到的相关DateUtil类的方法贴出来:
[java] view
plain copy
/**
* 日期相减(返回秒值)
* @param date Date
* @param date1 Date
* @return int
* @author
*/
public static Long diffDateTime(Date date, Date date1) {
return (Long) ((getMillis(date) - getMillis(date1))/1000);
}
public static long getMillis(Date date) {
Calendar c = Calendar.getInstance();
c.setTime(date);
return c.getTimeInMillis();
}
/**
* 获取 指定日期 后 指定毫秒后的 Date
*
* @param date
* @param millSecond
* @return
*/
public static Date getDateAddMillSecond(Date date, int millSecond) {
Calendar cal = Calendar.getInstance();
if (null != date) {// 没有 就取当前时间
cal.setTime(date);
}
cal.add(Calendar.MILLISECOND, millSecond);
return cal.getTime();
}
到这里就结束!
相关文章推荐
- java 用redis如何处理电商平台,秒杀、抢购超卖
- java 用redis如何处理电商平台,秒杀、抢购超卖
- java 用redis如何处理电商平台,秒杀、抢购超卖
- java 用redis如何处理电商平台,秒杀、抢购超卖
- 网站大规模并发处理方案:电商秒杀与抢购
- 电商的秒杀与抢购--大并发处理
- 阿里云Redis读写分离典型场景:如何轻松搭建电商秒杀系统
- 网站大规模并发处理方案:电商秒杀与抢购
- 阿里云Redis读写分离典型场景:如何轻松搭建电商秒杀系统
- 电商营销方式抢购,秒杀Redis原子出队列lpop方法作为剩余库存判断条件的实现方式(2)
- 阿里云Redis读写分离典型场景:如何轻松搭建电商秒杀系统
- Java-redis分布式锁 抢购秒杀系统 实现
- java使用mysql和redis如何解决“商品超卖”
- 阿里云Redis读写分离典型场景:如何轻松搭建电商秒杀系统
- 电商营销方式抢购,秒杀Redis原子出队列lpop方法作为剩余库存判断条件的实现方式(2)
- 电商的秒杀和抢购的实现
- 徐汉彬:Web系统大规模并发——电商秒杀与抢购