深入解析使用Redis实现分布式锁

294 阅读1分钟

平时我们在写代码的时候,通常都会这么去写,这种在普通的管理系统中是OK的,但是到并发高的情景下,就会出现问题,这里以jmeter压测为例就会出现问题。

public final static String REDIS_LOCK = "redis_lock";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String port;

    @GetMapping("/test")
    public void test() {
        String num = stringRedisTemplate.opsForValue().get(REDIS_VALUE);
        assert num != null;
        int value = Integer.parseInt(num);
        if (value > 0) {
            Long newNum = stringRedisTemplate.opsForValue().decrement(REDIS_VALUE);
            stringRedisTemplate.opsForValue().set(REDIS_VALUE, String.valueOf(newNum));
            System.out.println("库存还剩" + newNum + "件" + ",目前访问的节点是" + port);
        } else {
            System.out.println("卖完了!");
        }
    }
  • 在单节点应用中,我们可以加上JVM提供的内置锁(synchronizereentrantLock)等操作,即可以解决这个问题。
synchronized (this) {
    if (value > 0) {
        // 业务代码
    }
}
  • 如果将上面的程序部署两份,通过Nginx去调用,就算是加了内置锁,也是会出现问题的。

  • 这个时候就要用分布式锁了,这里用Redis实现分布式锁,先自己实现。

public final static String REDIS_LOCK = "redis_lock";
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, REDIS_LOCK);
if (!flag) {
    System.out.println("锁已经被占用!");
    return;
}
try {
    // 业务代码     
} finally {
    stringRedisTemplate.delete(REDIS_LOCK);
}

上面这种会有锁不释放的问题,可能服务挂了,程序还没执行到finally就挂了,这个时候就会出现锁不释放的问题

  • 解决上面的可以加上过期时间

注意添加过期时间和设置值的原子性

// 这个是保证原子性的
String redisValue = UUID.randomUUID().toString() + Thread.currentThread().getName().toString();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, redisValue, 10L, TimeUnit.SECONDS);

// 下面这两个是不具备原子性的
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, redisValue);
stringRedisTemplate.expire(REDIS_LOCK,10L,TimeUnit.SECONDS);
  • 就算是设置了过期时间,也还可能出现问题,当执行业务的时间超过设置的过期时间,这个时候业务代码还没有执行完,Redis就自动把锁给释放了,这个时候其他的线程就会跑进来抢到锁,而此时前面一个线程执行完了,会执行finally里面的删除锁操作,这个时候就会把别人的锁删除掉,通过下面的方式可以解决。
if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(redisValue)) {
    stringRedisTemplate.delete(REDIS_LOCK);
}
  • 但是删除锁的操作也要要求原子性,可以采用官方提供的lua脚本操作
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
        "    return redis.call(\"del\",KEYS[1])\n" +
        "else\n" +
        "    return 0\n" +
        "end";
jedis.eval(script, Arrays.asList(REDIS_LOCK), Arrays.asList(REDIS_VALUE));
  • 也可以通过Redis事务的方式实现,只是这种基本上不常用,应付面试
if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(redisValue)) {
for (; ; ) {
        // 监听某一个key,当被其他线程修改以后,就会退出然后重试
        stringRedisTemplate.watch(REDIS_LOCK);
        // 开启事务
        stringRedisTemplate.setEnableTransactionSupport(true);
        stringRedisTemplate.multi();
        stringRedisTemplate.delete(REDIS_LOCK);
        stringRedisTemplate.exec();
    }
}
  • 就算把上面的过期时间变大,也无法保证业务执行时间不会超过那个过期时间,这个时候需要开启一个续命线程,动态扩展过期时间,这个时候可以考虑使用Redisson,别人写的肯定比我们自己实现的要好,自己实现的在生产中可能会出现各种问题

  • 使用Redisson实现最终分布式锁

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.14.1</version>
</dependency>
@Bean
public Redisson redisson() {
    Config config = new Config();
    config.useClusterServers()
            .addNodeAddress("redis://172.17.81.71:31711");
    return (Redisson) Redisson.create(config);
}
@RestController
public class TestController {
    public final static String REDIS_VALUE = "redis_value";
    public final static String REDIS_LOCK = "redis_lock";
    @Autowired
    Redisson redisson;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;

    @GetMapping("/test")
    public void test() {
        RLock lock = redisson.getLock(REDIS_LOCK);
        lock.lock();
        try {
            String num = stringRedisTemplate.opsForValue().get(REDIS_VALUE);
            assert num != null;
            int value = Integer.parseInt(num);
            if (value > 0) {
                Long newNum = stringRedisTemplate.opsForValue().decrement(REDIS_VALUE);
                stringRedisTemplate.opsForValue().set(REDIS_VALUE, String.valueOf(newNum));
                System.out.println("库存还剩" + newNum + "件" + ",目前访问的节点是" + port);
            } else {
                System.out.println("卖完了!");
            }
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}