redis的依赖先引入

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

并发锁相关命令介绍

说到redis的并发锁就要先介绍一下这两个redis命令

SETNX key value

将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。

返回值
Integer reply, 特定值:

1 如果key被设置了
0 如果key没有被设置

demo

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis>

GETSET key value

自动将key对应到value并且返回原来key对应的value。如果key存在但是对应的value不是字符串,就返回错误。

demo

redis> INCR mycounter
(integer) 1
redis> GETSET mycounter "0"
"1"
redis> GET mycounter
"0"
redis>

那么下面开始进入redis锁的实现

处理流程

  • 1-尝试加锁
  • 2-加锁成功,处理业务
  • 3-解锁

加锁代码

这里做个demo介绍
假如已经加锁了,但是还没来得及释放锁,因为不可抗力因素,比如停电了,服务突然挂了,导致锁没释放
然后有2个A和B线程进来了,第一步判断加锁肯定失败了,因为之前那个锁没释放嘛,然后进入下面的判断,都获取到原先那个锁的值,并且判断出锁已经超时,
那么这个时候肯定A和B有一个先后顺序,因为getset是原子性的,假设A线程处理先设置值,那么这个时候它拿到的返回值是原先的值,进入判断成功获取锁,然后B线程也接着getSet后拿到了返回值,这个时候B拿到的返回值是A刚set进去的值,和最开始的那个超时key的value不一致,所以获取锁失败

package com.chan.wechatshop.service;

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 org.springframework.util.StringUtils;

@Component
@Slf4j
public class RedisLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 加锁
     * @param key
     * @param value 当前时间+超时时间
     * @return
     */
    public boolean lock(String key,String value){
        //这里的setIfAbsent即对应redis的setnx
        if(redisTemplate.opsForValue().setIfAbsent(key,value)){
            return true;
        }

        String currentValue = redisTemplate.opsForValue().get(key);
        //判断锁是否过期
        if(!StringUtils.isEmpty(currentValue) || Long.parseLong(currentValue) < System.currentTimeMillis()){
            //获取上一个过期时间
            String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
            if(!StringUtils.isEmpty(oldValue) && oldValue.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) {
            e.printStackTrace();
            log.error("[redis分布式锁] 解锁异常: {}",e.getMessage());
        }
    }
}

上线这种方式一般情况下是ok的,但是有一种特殊情况下解锁的时候会记录大量的
[redis分布式锁] 解锁异常

当一个锁过期的时候,2个线程同时进入加锁流程,因为锁因为其他原因过期了,然后没删除掉,此时进入锁超时判断流程,此时2个线程对于getAndSet肯定会有一个先后的执行顺序,因为其实原子性的.假设第一个线程getAndSet成功了,那么它可以进入方法return true从而加锁成功.第二个线程getAndSet后获得的之前的值是第一个线程set进去的值所以不会进入方法从而返回false.不过此时这个key在redis中存在的值已经是第二个线程set进去的值了,所以第一个线程在解锁的时候因为set进去的value是第二个线程set进去的值了,所以这里用第一个线程自己的value去解锁肯定失败.考虑到显示生产肯定有很多并发的情况,所以这里还是有点问题的.