当我并发测试时:
$ ab -n 500 -c 100 http://localhost:8080/seckill/1/
这TM肯定不行啊,这就超卖了,明明没这么多商品,结果还卖出去了。。。
二、synchronized处理并发
首先,synchronized的确是一个解决办法,而且也很简单,在方法前面加一个synchronized关键字。
但是通过压测,发现请求变的很慢,因为:
synchronized就用一个锁把这个方法锁住了,每次访问这个方法,只会有一个线程,所以这就是它导致慢的原因。通过这种方式,保证这个方法中的代码都是单线程来处理,不会出什么问题。
同时,使用synchronized还是存在一些问题的,首先,它无法做到细粒度的控制,比如同一时间有秒杀A商品和B商品的请求,都进入到了这个方法,虽然秒杀A商品的人很多,但是秒杀B商品的人很少,但是即使是买B商品,进入到了这个方法,也会一样的慢。
最重要的是,它只适合单点的情况。如果以后程序水平扩展了,弄了个集群,很显然,负载均衡之后,不同的用户看到的结果一定是五花八门的。
所以,还是使用更好的办法,使用redis分布式锁。
三、redis分布式锁
1、两个redis的命令
- SETNX key value
- GETSET key value
setnx key value 简单来说,setnx就是,如果没有这个key,那么就set一个key-value, 但是如果这个key已经存在,那么将不会再次设置,get出来的value还是最开始set进去的那个value.
网站中还专门讲到可以使用!SETNX加锁,如果获得锁,返回1,如果返回0,那么该键已经被其他的客户端锁定。
并且也提到了如何处理死锁。
getset key value 这个就更简单了,先通过key获取value,然后再将新的value set进去。
2、redis分布式锁的实现
我们希望的,无非就是这一段代码,能够单线程的去访问,因此在这段代码之前给他加锁,相应的,这段代码后面要给它解锁:
2.1 引入redis依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2.2 配置redis
spring: redis: host: localhost port: 6379
2.3 编写加锁和解锁的方法
package com.vito.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; /** * Created by VitoYi on 2018/4/5. */ @Component public class RedisLock { Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private StringRedisTemplate redisTemplate; /** * 加锁 * @param key 商品id * @param value 当前时间+超时时间 * @return */ public boolean lock(String key, String value) { if (redisTemplate.opsForValue().setIfAbsent(key, value)) { //这个其实就是setnx命令,只不过在java这边稍有变化,返回的是boolea return true; } //避免死锁,且只让一个线程拿到锁 String currentValue = redisTemplate.opsForValue().get(key); //如果锁过期了 if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) { //获取上一个锁的时间 String oldValues = redisTemplate.opsForValue().getAndSet(key, value); /* 只会让一个线程拿到锁 如果旧的value和currentValue相等,只会有一个线程达成条件,因为第二个线程拿到的oldValue已经和currentValue不一样了 */ if (!StringUtils.isEmpty(oldValues) && oldValues.equals(currentValue)) { return true; } } return false; } /** * 解锁 * @param key * @param value */ public void unlock(String key, String value) { try { String currentValue = redisTemplate.opsForValue().get(key); if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) { redisTemplate.opsForValue().getOperations().delete(key); } } catch (Exception e) { logger.error("『redis分布式锁』解锁异常,{}", e); } } }
为什么要有避免死锁的一步呢?
假设没有『避免死锁』这一步,结果在执行到下单代码的时候出了问题,毕竟操作数据库、网络、io的时候抛了个异常,这个异常是偶然抛出来的,就那么偶尔一次,那么会导致解锁步骤不去执行,这时候就没有解锁,后面的请求进来自然也或得不到锁,这就被称之为死锁。
而这里的『避免死锁』,就是给锁加了一个过期时间,如果锁超时了,就返回true,解开之前的那个死锁。
2.4 下单代码中引入加锁和解锁,确保只有一个线程操作
@Autowired private RedisLock redisLock; @Override @Transactional public String seckill(Integer id)throws RuntimeException { //加锁 long time = System.currentTimeMillis() + 1000*10; //超时时间:10秒,最好设为常量 boolean isLock = redisLock.lock(String.valueOf(id), String.valueOf(time)); if(!isLock){ throw new RuntimeException("人太多了,换个姿势再试试~"); } //查库存 Product product = productMapper.findById(id); if(product.getStock()==0) throw new RuntimeException("已经卖光"); //写入订单表 Order order=new Order(); order.setProductId(product.getId()); order.setProductName(product.getName()); orderMapper.add(order); //减库存 product.setPrice(null); product.setName(null); product.setStock(product.getStock()-1); productMapper.update(product); //解锁 redisLock.unlock(String.valueOf(id),String.valueOf(time)); return findProductInfo(id); }
这样再来跑几次压测,就不会超卖了:
封装-----
/**
* @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;
}
/**
* @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;
}
});
}
/**
* @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类的方法贴出来:
/**
* 日期相减(返回秒值)
* @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();
}
新补充:
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.TimeoutUtils;
import org.springframework.stereotype.Component;
import cn.mindmedia.jeemind.framework.utils.redis.RedisUtils;
import cn.mindmedia.jeemind.utils.DateUtils;
/**
* @ClassName: LockRetry
* @Description: 此功能只用于促销组
* @date 2017年7月29日 上午11:54:54
*/
@SuppressWarnings("rawtypes")
@Component("lockRetry")
public class LockRetry {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private RedisTemplate redisTemplate;
/**
*
* @Title: retry
* @Description: 重入锁
* @param @param lock 名称
* @param @param expire 锁定时长(秒),建议10秒内
* @param @param num 取锁重试试数,建议不大于3
* @param @param interval 重试时长
* @param @param forceLock 强制取锁,不建议;
* @param @return
* @param @throws Exception 设定文件
* @return Boolean 返回类型
* @throws
*/
@SuppressWarnings("unchecked")
public Boolean retryLock(final String lock, final int expire, final int num, final long interval, final boolean forceLock) throws Exception {
Date lockValue = (Date) redisTemplate.opsForValue().get(lock);
if (forceLock) {
RedisUtils.remove(lock);
}
if (num <= 0) {
if (null != lockValue && lockValue.getTime() >= (new Date().getTime())) {
logger.debug(String.valueOf((lockValue.getTime() - new Date().getTime())));
Thread.sleep(lockValue.getTime() - new Date().getTime());
RedisUtils.remove(lock);
return retryLock(lock, expire, 1, interval, forceLock);
}
return false;
} else {
return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
boolean locked = false;
byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtils.getDateAdd(null, expire, Calendar.SECOND));
byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);
logger.debug(lockValue.toString());
locked = connection.setNX(lockName, lockValue);
if (locked)
return connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.SECONDS));
else {
try {
Thread.sleep(interval);
return retryLock(lock, expire, num - 1, interval, forceLock);
} catch (Exception e) {
e.printStackTrace();
return locked;
}
}
}
});
}
}
}
/**
*
* @Title: getDateAddMillSecond
* @Description: (TODO)取将来时间
* @param @param date
* @param @param millSecond
* @param @return 设定文件
* @return Date 返回类型
* @throws
*/
public static Date getDateAdd(Date date, int expire, int idate) {
Calendar calendar = Calendar.getInstance();
if (null != date) {// 默认当前时间
calendar.setTime(date);
}
calendar.add(idate, expire);
return calendar.getTime();
}
/**
* 删除对应的value
* @param key
*/
public static void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 判断缓存中是否有对应的value
* @param key
* @return
*/
public static boolean exists(final String key) {
return stringRedisTemplate.hasKey(key);
}
private static StringRedisTemplate stringRedisTemplate = ((StringRedisTemplate) SpringContextHolder.getBean("stringRedisTemplate"));