为什么需要分布式锁
在日常开发中,很多业务场景必须保证原子性。举几个例子:
- 支付订单的操作,就不允许同一个订单,被同时支付,否则会产生错误的数据。
- 拍卖一套房子的下单操作,商品只有一个,那只能一个人下单成功。
如果你只有一台服务器,只运行一个Java程序,那么可以使用Java语言自身的一些锁来实现原子性。但如果我们有多台服务器,甚至不同服务器上跑的是不同的语言。那这时候,我们就需要一个跨平台、跨语言的加锁方式。redis就是其中最方便的一种。
核心操作和原理
使用redis实现并发锁,主要是靠两个redis的命令:setnx和getset。
- setnx的作用是,当一个key不存在的时候,给它赋值。如果key存在或赋值失败,都会返回错误。
- getset的作用是,先获取一个key的值,然后再给这个key赋新的值,该命令有原子性。
那我们的设计思路就是:
- 先用setnx初步获取锁(set当前的时间戳),如果取不到,那么有两种可能,要么是锁被其他线程持有,要么是其他线程使用完锁后,没有正确释放。
- 所以这个时候,我们需要验证这个锁是否过期。就是把setnx的值拿出来(一个时间戳),和当前时间戳求差,看看超时没有(超时时间是自己设定的)
- 如果锁超时了,我们需要释放它,让它能重新工作。但第二步的操作,不是原子性的。可能有多个线程发现这个锁过期了,都想释放它。这时候,就需要getset这个原子操作,来保证只有一个线程成功。
核心代码
@Service
public class RedisLockService {
@Autowired
private RedisService redisService;
/**
* 获取一个Redis分布式锁
* @param lockKey 锁的Key,全局不可重复
* @param lockExpire 锁超时时间,单位毫秒
* @return
*/
public boolean getLock(String lockKey, long lockExpire) {
String redisKey = BaseCommonConfig.REDIS_LOCK_KEY + lockKey;
if (!redisService.setnx(redisKey, String.valueOf(System.currentTimeMillis()))) {
//没有拿到锁,但有可能是上一个加锁的人忘了释放锁。所以下面验证锁是否超时。
String lockString = redisService.getString(redisKey);
if (lockString == null) {
//前面setnx时,该值还存在,现在不存在了。要么是自然过期了,要么是被别人删掉,准备重新加锁了。稳妥起见,这里返回false
return false;
}
long timestamp = Long.parseLong(lockString);
if (System.currentTimeMillis() - timestamp > lockExpire) {
//锁已经超时
//先get值,再set值。原子操作,确保不会多个线程进入后面的逻辑
String oldTimestamp = redisService.setGet(redisKey, String.valueOf(System.currentTimeMillis()));
if (oldTimestamp != null && oldTimestamp.equals(lockString)) {
//如果get不到值,或者get到的值不是前面取出来那个了,说明这个锁已经被别的线程占用了。
//第二次锁竞争成功
redisService.setExpireMills(redisKey, lockExpire);
return true;
} else {
return false;
}
} else {
return false;
}
}
redisService.setExpireMills(redisKey, lockExpire);
return true;
}
/**
* 删除锁,释放锁
*
* @param lockKey
*/
public void delLock(String lockKey) {
String redisKey = BaseCommonConfig.REDIS_LOCK_KEY + lockKey;
redisService.del(redisKey);
}
}
上面的代码使用了一个RedisService的类,里面主要是简单封装了一下redis的操作,你可以替换为自己的service。代码如下:
@Service
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedisTemplate<String, Object> redisTemplate;
public Boolean setnx(String key, String value) {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
return ops.setIfAbsent(key, value);
}
public String setGet(String key, String value) {
return stringRedisTemplate.opsForValue().getAndSet(key, value);
}
public Long incr(String key) {
return stringRedisTemplate.opsForValue().increment(key);
}
public Long incr(String key,long val) {
return stringRedisTemplate.opsForValue().increment(key,val);
}
public Long decr(String key) {
return stringRedisTemplate.opsForValue().decrement(key);
}
public Long decr(String key,long val) {
return stringRedisTemplate.opsForValue().decrement(key,val);
}
public Long setPutString(String key, String value) {
return stringRedisTemplate.opsForSet().add(key, value);
}
public Boolean setExist(String key, String member) {
return stringRedisTemplate.opsForSet().isMember(key, member);
}
public Set<String> setList(String key) {
return stringRedisTemplate.opsForSet().members(key);
}
public Long setSize(String key) {
return stringRedisTemplate.opsForSet().size(key);
}
/**
* 逐渐废弃没有过期时间的Redis put操作
* @param key
* @param value
*/
@Deprecated
public void putString(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
/**
* s为单位。
*
* @param key
* @param value
* @param time
*/
public void putString(String key, String value, long time) {
stringRedisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}
public Boolean zPutString(String key, String value, long time) {
return stringRedisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
}
public void putString(String key, String value, long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, value, time, unit);
}
public String getString(String key) {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
return ops.get(key);
}
public void putObject(String key, Object value, long time, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, time, unit);
}
public Object getObject(String key) {
return redisTemplate.opsForValue().get(key);
}
public Boolean exist(String key) {
return stringRedisTemplate.hasKey(key);
}
/**
* key有效时间
*
* @param key
* @return
*/
public Long expire(String key) {
return stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
}
public Boolean changeExpire(String key, long time) {
return stringRedisTemplate.expire(key, time, TimeUnit.SECONDS);
}
public Boolean del(String key) {
return stringRedisTemplate.delete(key);
}
public void hset(String key, String field, String value) {
HashOperations<String, String, String> ops = stringRedisTemplate.opsForHash();
ops.put(key, field, value);
}
public String hget(String key, String field) {
HashOperations<String, String, String> ops = stringRedisTemplate.opsForHash();
return ops.get(key, field);
}
public Map<String, String> hmget(String key) {
HashOperations<String, String, String> ops = stringRedisTemplate.opsForHash();
return ops.entries(key);
}
public void hmset(String key, HashMap<String,String> data) {
HashOperations<String, String, String> ops = stringRedisTemplate.opsForHash();
ops.putAll(key,data);
}
public void hdel(String key, String field) {
HashOperations<String, String, String> ops = stringRedisTemplate.opsForHash();
ops.delete(key, field);
}
public void sadd(String key, String value) {
stringRedisTemplate.opsForSet().add(key, value);
}
public Boolean setIsMember(String key,String val){
return stringRedisTemplate.opsForSet().isMember(key,val);
}
public Long ttl(String key) {
return stringRedisTemplate.getExpire(key);
}
public void setExpire(String key, long time) {
stringRedisTemplate.expire(key, time, TimeUnit.SECONDS);
}
public void setExpireMills(String key, long time) {
stringRedisTemplate.expire(key, time, TimeUnit.MILLISECONDS);
}
public List<String> mget(List<String> keys) {
return stringRedisTemplate.opsForValue().multiGet(keys);
}
}
交个朋友
以上代码有任何疑问,可以点击右侧边栏联系作者。收费5毛~交个朋友,欢迎来撩!