Redis分布式锁(一)
分布式锁场景:
- 互联网秒杀
- 抢优惠券
1、两个redis的命令
setnx key value
只在键key不存在的情况下,将键key的值设置为value。
若是键key已存在值,则此命令不做任何操作。
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;
@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);
上面的这个代码有什么问题吗?
...
1.当一个线程获取到锁之后,此时其他线程被阻塞,执行下面的业务逻辑代码时发生异常,导致没有继续执行释放锁操作,会导致死锁。
解决方案:增加异常捕获机制(try{...}finally{...}),解锁操作放进finally代码块中,无论是否发生异常都会执行解锁操作。
2.当一个线程获取到锁之后,此时其他线程被阻塞,执行下面的业务逻辑代码时程序宕机了,导致没有继续执行finally代码块中的释放锁操作,一样会导致死锁。
解决方案:给redis锁增加一个超时时间,就算没执行释放锁的操作,redis到期一样会清掉。
但是,这个锁的超时时间如何设置是一个问题,比如,
我们整个程序执行需要15s,而我们给redis锁的超时时间设了10s,也就是说,当线程执行到10s的时候,我们设置的锁到期了,会被redis自动清掉,这时候另一个线程过来尝试获取锁,发现锁是空着的,立马就获取到了锁,且再次设置了过期时间;
此时第一个线程继续执行5s,执行最后一步释放锁操作,而此时的锁是第二个线程加上的,不是第一个线程加的锁;
上面的问题可以简化为:
3.一个线程设置的锁被另一个线程释放了。
解决方案:每个线程自己设置的锁只能自己释放,线程在添加锁的时候,随机获取一个uuid,将这个uuid设置到自己的value中,在释放锁的时候,将uuid和redis存的value进行比较,一样才能释放锁。
下面附上工作中使用的Redis锁:
package com.supaur.oneid.ext.lock.redis
import com.supaur.oneid.ext.lock.api.ILock;
import com.supaur.oneid.ext.lock.autoconfigure.ExtLockProperties;
import com.supaur.oneid.ext.lock.redis.dto.RedisValue;
import com.supaur.oneid.ext.time.GMTs;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.UUID;
@Slf4j
public class RedisLock implements ILock {
private static final String CACHE_PREFIX = RedisLock.class.getName();
private final RedisTemplate<String, Serializable> redisTemplate;
private final ExtLockProperties properties;
private Converter<Object, byte[]> serializer = new SerializingConverter();
private Converter<byte[], Object> deserializer = new DeserializingConverter();
public RedisLock(RedisTemplate redisTemplate, ExtLockProperties extLockProperties) {
this.redisTemplate = redisTemplate;
this.properties = extLockProperties;
@Override
public Boolean lock(String key) {
try {
if (key == null) {
return false;
String uuid = UUID.randomUUID().toString();
final byte[] cacheKey = (CACHE_PREFIX + key).getBytes();
Long expire = properties.getExpire().get(key) == null ? ExtLockProperties.DEFAULT_EXPIRE : properties.getExpire().get(key);
Long expiredOn = GMTs.TIME_MS.apply(expire);
return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
Boolean success;
Boolean result = connection.setNX(cacheKey, serializer.convert(new RedisValue(uuid, expiredOn)));
byte[] value = connection.get(cacheKey);
// set的值被别人删除了
if (value == null) {
success = false;
break;
RedisValue redisValue = (RedisValue) deserializer.convert(value);
if (result) {
// 判断是否为自己set的value,可能存在自己设置的key被别人delete且set
if (!uuid.equals(redisValue.getUuid())) {
success = false;
break;
connection.pExpire(cacheKey, expire);
success = true;
break;
// 如果已经过期,删除key,并且重新set
if (redisValue.getExpiredOn() < GMTs.NOW_MS.get()) {
unlock(key);
} else {
// 如果key没有过期,set失败,返回false
success = false;
break;
} while (true);
return success;
} catch (Exception ex) {
log.error("Unable to retrieve the lock for {},cause:{}", key, ex);
return false;
@Override
public Boolean unlock(String key) {
if (key == null) {
return false;
final byte[] cacheKey = (CACHE_PREFIX + key).getBytes();