添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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();