一、背景
在天猫、京东、苏宁等等电商网站上有很多秒杀活动,例如在某一个时刻抢购一个原价1999现在秒杀价只要999的手机时,会迎来一个用户请求的高峰期,可能会有几十万几百万的并发量,来抢这个手机,在高并发的情形下会对数据库服务器、文件服务器、应用服务器造成巨大的压力,严重时甚至宕机了。另一个问题是,秒杀的东西都是有量的,例如一款手机只有10台的量秒杀,那么,在高并发的情况下,成千上万条数据更新数据库(例如10台的量被人抢一台就会在数据集某些记录下减1),那这个时候的先后顺序是很乱的,很容易出现10台的量,抢到的人就不止10个这种严重的问题。
那么,这个高并发+限量
的问题我们该如何去解决呢?使用任务队列和分布式锁
,本节主要阐述基于redis的分布式锁
实现思路。
二、设计思路
1、基本思路
主要用到的是redis函数
setNX()
,这个应该是实现分布式锁最主要的函数。首先是将某一任务标识名(lockKey)作为键存到redis里,并为其设个过期时间,如果是还有lockKey请求过来,先是通过setNX()
看看是否能将lockKey插入到redis里,可以的话就返回true,不可以就返回false。
2、锁过期时间
为避免特殊原因导致获得的锁无法释放,在加锁成功后,通过redis函数
expire()
给锁赋予一个生存时间,超出生存时间锁会被自动释放。
三、实现
1、获取锁
/**
* 获取redis锁
*
* @param jedisTemplate 没啥好说的
* @param lockName 锁的唯一名称
* @param lockTimeout 锁过期时间(单位:秒)
* @param acquireTimeout 请求锁的超时时间 (单位:秒)
* @param retryDuration 请求锁的重试间隔时间 (单位:毫秒)
* @return
*/
@Deprecated
public static Boolean tryLock(final RedisTemplate jedisTemplate, final String lockName, final int lockTimeout, final int acquireTimeout, final long retryDuration) {
if (isEmpty(lockName) || lockTimeout <= 0) {
return false;
}
final String lockKey = lockName;
String identifier = UUID.randomUUID().toString();
Calendar atoCal = Calendar.getInstance();
atoCal.add(Calendar.SECOND, acquireTimeout);
Date atoTime = atoCal.getTime();
log.info("Try to acquire the lock. lockKey={},acquireTimeout={}s,lockTimeout={}s", lockKey, acquireTimeout, lockTimeout);
while (true) {
//开始获取锁
boolean acquiredLock = (boolean) jedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.setNX(jedisTemplate.getStringSerializer().serialize(lockKey), jedisTemplate.getStringSerializer().serialize(identifier));
}
});
if (acquiredLock) {//成功获取锁后,设置锁的过期时间,并返回ID
jedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.expire(jedisTemplate.getStringSerializer().serialize(lockKey), lockTimeout);
}
});
log.info("acquired lock. lockKey={}", lockKey);
return true;
} else { //如果获取失败,则重试获取锁,或者直接返回null
log.info("Retry to acquire the lock. lockKey={},acquireTimeout={}s,lockTimeout={}s", lockKey, acquireTimeout, lockTimeout);
if (acquireTimeout < 0)
return false;
else {
try {
log.info("wait 1000 milliseconds before retry. lockKey={}", lockKey);
Thread.sleep(retryDuration);
} catch (InterruptedException ex) {
}
}
if (new Date().after(atoTime)) {
break;
}
}
}
return false;
}
2、释放锁
public static void unLock(final RedisTemplate jedisTemplate, final String lockName) {
if (isEmpty(lockName)) {
return;
}
final String lockKey = lockName;
jedisTemplate.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.del(jedisTemplate.getStringSerializer().serialize(lockKey));
return null;
}
});
}
3、检查锁状态
//检查锁状态,锁住返回true,否则false
public static Boolean checkWhetherLockExists(final RedisTemplate jedisTemplate, final String lockName) {
if (isEmpty(lockName)) {
return false;
}
final String lockKey = lockName;
boolean lockExists = (boolean) jedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
byte[] bytes = connection.get(jedisTemplate.getStringSerializer().serialize(lockKey));
if(null != bytes){
return true;
} else {
return false;
}
}
});
return lockExists;
}