Resumen de rutina de bloqueo distribuido

Estoy participando en el "Programa de Vela · Nuggets"

¿Qué es un bloqueo distribuido?

Primero revisemos qué es un bloqueo El bloqueo es una solución a la inconsistencia de datos causada por múltiples subprocesos que operan el mismo recurso al mismo tiempo. Por lo general, los bloqueos que usan las personas son bloqueos independientes, como synchronizedpalabras clave y ReentrantLockbloqueos, que son solo bloqueos dentro del mismo proceso. Ahora, muchos servicios tendrán más de una instancia, por lo que el bloqueo de una sola máquina es inútil en absoluto. Entonces se introdujo el concepto de bloqueo distribuido.

La función del bloqueo distribuido es exactamente la misma que la del bloqueo independiente, excepto que, por lo general, debe implementarlo un servicio de terceros. Los principales servicios de terceros tienen redisy mysql.

Expliquemos los dos métodos de implementación y las diferencias uno por uno, así como el mejor método en mi opinión.

Redis implementa bloqueos distribuidos

Redis generalmente admite una mayor concurrencia y proporciona un comando atómico, por lo que es adecuado como bloqueo distribuido:

SET key value NX PX 30000
复制代码

Este comando significa establecer un valor clave de tipo cadena en redis, que caducará automáticamente después de 30 segundos , y se establecerá correctamente solo cuando la clave no exista. Esto está bastante en línea con nuestros requisitos para bloqueos distribuidos. Dado que redis es de un solo subproceso, podemos realizar esta operación en el lado del cliente, y garantizará que solo una instancia se configure correctamente. ¿Significa que el bloqueo es exitoso? ¿pronto? Parte del código es el siguiente:

// 仅当key不存在才会设置成功,通常是将key设置为需要操作的资源唯一id,
// 例如,我们需要秒杀商品,key就设置为商品id
// 而 value一般设置为随机数,来保证释放锁的时候是当前线程持有。我这里使用【hutool】工具生成了16位随机字符串
// 过期时间也需要设置,因为如果该线程出现异常,就会导致资源无法释放,造成其他线程永远拿不到锁了
String randomString = RandomUtil.randomString(16);
//此方法会返回一个结果来表示是否操作成功
Boolean result = redisTemplate.opsForValue().setIfAbsent("productId", randomString);
//加锁成功
if (Boolean.TRUE.equals(result)) {
    try {
        // 业务处理
    } catch (Exception ignored) {
        // 回滚事务
    } finally {
        // 判断该锁是否当前线程持有,是才会释放
        if (redisTemplate.opsForValue().get("productId").equals(randomString)) {
            //提交事务
            //释放锁
            redisTemplate.delete("productId");
        } else {
            //业务执行时间过长导致锁自动失效,此时需要释放资源,如回滚事物等
        } 
    }
}
//加锁失败
else {
    // 可以返回服务器繁忙,请稍后再试之类的友好提示
}
复制代码

Algunos consejos sobre transacciones, en términos generales, si el método actual implica 2 o más operaciones que modifican datos, debe usar transacciones

En términos generales, para la concurrencia general, el esquema anterior es completamente suficiente, sin embargo, todavía tiene algunos defectos:

  1. Juzgar si el hilo actual retiene el bloqueo y liberarlo no es una operación atómica. Si se considera que el bloqueo está retenido por el hilo actual, caducará en el próximo segundo, y otros hilos lo retendrán en esta vez, pronto se lanzarán otros subprocesos. ¿Mantener el candado?
  2. Hay problemas similares al enviar cosas

Para resolver este problema, podemos usar el script lua para realizar la operación atómica de liberar el bloqueo:

//释放锁的时候判断了锁是否当前线程持有
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
复制代码

El script lua puede garantizar que juzguemos si el valor es igual a nuestra expectativa, y solo cuando sea igual se liberará el recurso. Y es una operación atómica.

Sin embargo, esto se ve hermoso y resuelve el problema 1 anterior, pero el problema 2 aún no se puede resolver porque las transacciones y las operaciones redis no son operaciones atómicas. El script lua obviamente no puede ayudarnos a enviar transacciones mysql, entonces, ¿qué debemos hacer?

下面就推荐大名鼎鼎的redisson,它是一个redis客户端,主要支持一些分布式相关的工具,其中就有分布式锁。

说白了,上述两个问题,我们已经解决了其中一个了,采用了lua脚本解决,而另外一个问题,根本原因是因为自动过期时间设置多大问题。

那么,我们应该设置多长时间呢?

首先我们应该尽可能的设置一个保证在业务能够正常执行结束的范围。

但是,其实不管我们设置多少,理论上来说都不合适,因为你无法保证业务代码执行的具体时间。倘若设置小了,导致业务执行结束后锁过期,还要额外进行回滚操作,设置大了,可能导致其他线程阻塞时间过长。所以,这个时间怎么设置都不好使,总会有瑕疵。

redisson采用了看门狗设置,也就是会起一个守护线程,来监测这个线程是否释放锁,如果此线程一直在活动,且过期时间快要结束,看门狗机制就会自动续期。

所以,看门狗机制保证了,线程持有锁后,只要线程还在活跃,且锁未释放,锁会永不过期,有人看到这里可能会怀疑了,永不过期? ,那么岂不是跟没设置过期时间没啥两样,哈哈,并不是,我说的只是理论上永不过期,实际上我们的代码终究会执行结束(除非写了死循环)。

redisson释放锁的时候同样采用了lua脚本的方式判断是否当前锁持有。

最佳实践代码如下:

适合并发一般的情况:

RLock lock = redissonClient.getLock("productId");
//会一直阻塞去获取锁,直到成功,默认30秒过期,会自动续期
lock.lock();
try {
    //执行业务代码
    //正常执行,然后提交事务,这里锁会一直续期,所以不用担心锁会自动过期
} catch (Exception ignored) {
    //异常,回滚事务
} finally {
    //释放锁,需要判断一下是否当前线程持有
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}
复制代码

适合并发较高的情况:

RLock lock = redissonClient.getLock("productId");
//会一直阻塞去获取锁,直到成功,但5秒还未获取锁,会返回结果,锁默认30秒过期,会自动续期
// ture代表获取锁成功,否则失败
if (lock.tryLock(5, TimeUnit.SECONDS)) {
    try {
        //执行业务代码
        //正常执行,然后提交事务,这里锁会一直续期,所以不用担心锁会自动过期导致事务提交后才过期
    } catch (Exception ignored) {
        //异常,回滚事务
    } finally {
        //释放锁,需要判断一下是否当前线程持有
        //注意:这里如果不判断也是可以的,只不过会抛出异常
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
} else {
    // 可以返回服务器繁忙,请稍后再试之类的友好提示
}
复制代码

关于redisson的看门狗失效情况:

// 没有Watch Dog ,10s后锁释放
lock.lock(10, TimeUnit.SECONDS);
// 没有Watch Dog ,10s后锁释放,尝试获取100s
lock.tryLock(100, 10, TimeUnit.SECONDS);
复制代码

redisson默认锁过期时间为30s,只要设置了过期时间,看门狗机制就会失效

MYSQL实现分布式锁

mysql实现分布式锁的方式最为简单,我们可以利用mysql主键唯一的性质,将新增数据这一动作的成功与否作为获取锁的结果。对于实现自动过期。我们可以增加字段来实现,增加一个过期时间字段和创建时间字段。

释放锁就是删除数据即可,如果锁支持自动失效,需要在释放锁时添加相应条件以防止释放锁时刚好自动失效。

Supongo que te gusta

Origin juejin.im/post/7145637351765573662
Recomendado
Clasificación