redis 实现分布式锁

199 阅读3分钟

这是我参与更文挑战的第29天,活动详情查看: 更文挑战

什么是分布式锁?

首先要提到与分布式锁相对应的是线程锁、进程锁。

线程锁:主要给方法和代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

进程锁:控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。

分布式锁:多个进程不在同一个系统,用分布式锁控制多个进程对资源的访问。

分布式锁要求:

高性能,高可用,避免死锁

分布式锁实现:

1基于数据库实现分布式锁

2基于缓存(Redis等)实现分布式锁

3基于Zookeeper实现分布式锁

实现加锁解锁

import com.google.common.base.Strings;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class RedisLockHelper {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 加锁
     *
     * @param targetId  targetId - 商品的唯一标志
     * @param timeStamp 当前时间+超时时间 也就是时间戳
     * @return
     */
    public boolean lock(String targetId, String timeStamp) {
        if (stringRedisTemplate.opsForValue().setIfAbsent(targetId, timeStamp, 10, TimeUnit.SECONDS)) {
            // 对应setnx命令,可以成功设置,也就是key不存在
            return true;
        }

        // 判断锁超时 - 防止原来的操作异常,没有运行解锁操作  防止死锁
        String currentLock = stringRedisTemplate.opsForValue().get(targetId);
        // 如果锁过期 currentLock不为空且小于当前时间
        if (!Strings.isNullOrEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()) {
            // 获取上一个锁的时间value 对应getset,如果lock存在
            String preLock = stringRedisTemplate.opsForValue().getAndSet(targetId, timeStamp);

            // 假设两个线程同时进来这里,因为key被占用了,而且锁过期了。获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
            // 而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的timeStamp已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
            if (!Strings.isNullOrEmpty(preLock) && preLock.equals(currentLock)) {
                // preLock不为空且preLock等于currentLock,也就是校验是不是上个对应的商品时间戳,也是防止并发
                return true;
            }
        }
        return false;
    }


    /**
     * 解锁
     *
     * @param target
     * @param timeStamp
     */
    public void unlock(String target, String timeStamp) {
        try {
            String currentValue = stringRedisTemplate.opsForValue().get(target);
            if (!Strings.isNullOrEmpty(currentValue) && currentValue.equals(timeStamp)) {
                // 删除锁状态
                stringRedisTemplate.opsForValue().getOperations().delete(target);
            }
        } catch (Exception e) {
            log.error("警报!警报!警报!解锁异常{}", e);
        }
    }
}

测试

import com.redis.demo.util.RedisLockHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class RedisController {

    @Autowired
    RedisLockHelper redisLockHelper;

    int surplusCount = 2;

    /**
     * 超时时间 5s
     */
    private static final int TIMEOUT = 5*1000;

    @RequestMapping(value = "/seckilling")
    public String Seckilling(String targetId){
        //加锁
        long time = System.currentTimeMillis() + TIMEOUT;
        if(!redisLockHelper.lock(targetId,String.valueOf(time))){
            return "排队人数太多,请稍后再试.";
        }

//        int surplusCount = 1;
        // 查询该商品库存,为0则活动结束 e.g. getStockByTargetId
        if(surplusCount==0){
            return "活动结束.";
        }else {
            // 下单 e.g. buyStockByTargetId

            //减库存 不做处理的话,高并发下会出现超卖的情况,下单数,大于减库存的情况。虽然这里减了,但由于并发,减的库存还没存到map中去。新的并发拿到的是原来的库存
            surplusCount =surplusCount-1;
            try{
                Thread.sleep(200);//模拟减库存的处理时间
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            // 减库存操作数据库 e.g. updateStockByTargetId

            // buyStockByTargetId 和 updateStockByTargetId 可以同步完成(或者事物),保证原子性。
        }

        //解锁
        redisLockHelper.unlock(targetId,String.valueOf(time));

        return "恭喜您,秒杀成功。";
    }
}

以上加锁方法适用于redis单实例,主备等 ,不适合redis集群