RedisSon高并发分布式锁实战

news/2024/11/17 17:38:25/

Redis高并发分布式锁实战

1.分布式场景下的synchronized失效的问题–用redis实现分布式锁

synchronized是通过monitor实现的jvm级别的锁,如果是分布式系统,跑在不同的虚拟机上的tomcat上,会导致synchronized无法锁住对象 ----------- 需要分布式锁 redis

SET、SETEX、SETNX

SET key value
含义:

     SET KEY value  V-K相同的K 后写的覆盖先写的

image-20230608165040260

SETEX key seconds value
该命令相当于将下面两行操作合并为一个原子操作

SET key value
EXPIRE key seconds # 设置生存时间
含义(setex = set expire):

          SET KEY value  V-K 设置生命周期 相同的K 后写的覆盖先写的

image-20230608165944711

image-20230608170022805

SETNX key value
含义(setnx = SET if Not eXists):

       SET KEY value  V-K  ,key 不存在返回1 表示成功,key存在返回0SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:

       设置成功,返回 1 。设置失败,返回 0 。

image-20230608171143353

2.redis实现分布式锁

@RequestMapping("/redis-001")
public String redis001() {String key = "redis-001";Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001");if (!result) {return "error_code";}int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + "");System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}stringRedisTemplate.delete(key);return "end";
}

在 stringRedisTemplate.delete(key) 释放锁之前会有业务代码块,若出现异常抛出,则不能执行关锁的代码块

@RequestMapping("/redis-001")
public String redis001() {String key = "redis-001";Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001");if (!result) {return "error_code";}try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + "");System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}} finally {stringRedisTemplate.delete(key);}return "end";
}

在程序执行的任意时刻都有可能应为不可抗力因素突然终止,重启、宕机导致不能执行到finally代码块,所以必须要设置超时时间

    @RequestMapping("/redis-001")public String redis001() {String key = "redis-001";Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001");stringRedisTemplate.expire(key,2000 ,TimeUnit.MILLISECONDS);if (!result) {return "error_code";}try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + "");System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}} finally {stringRedisTemplate.delete(key);}return "end";}

redis设置的时候需要保证原子性

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001");
stringRedisTemplate.expire(key,2000 ,TimeUnit.MILLISECONDS);

解决方案

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, clintId,2000 ,TimeUnit.MILLISECONDS);

若T1的锁设置失效时间20s,但是T1执行20s没有完成,此时T2可以获得锁,T1执行会在finally代码块中释放T2加的锁

    @RequestMapping("/redis-001")public String redis001() {String key = "redis-001";//不满足原子性
//        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001");
//        stringRedisTemplate.expire(key,2000 ,TimeUnit.MILLISECONDS);String clintId = UUID.randomUUID().toString();//Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001",2000 ,TimeUnit.MILLISECONDS);Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, clintId,2000 ,TimeUnit.MILLISECONDS);if (!result) {return "error_code";}try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + "");System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}} finally {if (clintId.equals(stringRedisTemplate.opsForValue().get(key))){stringRedisTemplate.delete(key);}}return "end";}
finally {if (clintId.equals(stringRedisTemplate.opsForValue().get(key))){stringRedisTemplate.delete(key);}}

上述代码来确定是否是自己的锁,是没有原子性的,使用redisson(lua)来解决这个问题

Redisson

image-20230613163801017

Redis 自 2.6 版本开始支持 Lua 脚本,是将所有操作打包成原子操作的一种机制。使用 Lua 脚本可以对 Redis 数据库进行复杂的操作,比如多个命令组合执行、避免分布式事务中的竞态条件等。

Lua 脚本在 Redis 中的原理是:将脚本发送到 Redis 服务器时,Redis 会先对脚本进行语法检查和编译,然后将编译后的字节码缓存起来并返回一个 SHA1 校验和。之后客户端每次需要执行这个脚本时,只需要将 SHA1 校验和发送给 Redis 服务器,Redis 通过校验和即可直接获取缓存中的字节码,避免了每次解析和编译 Lua 脚本的开销。

Lua 脚本的好处有以下几点:

  1. 原子性:Lua 脚本是 Redis 支持的最完整的事务形式,因为它们在 Redis 服务器上作为一个单独的脚本条目执行,因此能够保证所有操作的原子性。

  2. 灵活性:Lua 脚本方便对于 Redis 数据库进行复杂的操作,比如批量操作等。

  3. 性能:由于 Redis 会对 Lua 脚本进行预编译并缓存字节码,因此当相同的脚本被多次执行时,可以避免每次解析和编译脚本带来的开销。而且,Lua 脚本在 Redis 服务器中以单线程运行,相比于多线程,这样可以减少线程切换、锁等开销。

    image-20230615110608304

总之,Redis Lua 脚本具有良好的性能、灵活性和原子性,使得 Redis 支持更复杂、更安全地操作数据,提高了 Redis 在实际应用中的可靠性和稳定性。

Lua脚本语法

image-20230615143634666

image-20230615145300265

Redisson.lock()

image-20230615150628987

 <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {internalLockLeaseTime = unit.toMillis(leaseTime);return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,"if (redis.call('exists', KEYS[1]) == 0) then " +"redis.call('hset', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"return redis.call('pttl', KEYS[1]);",Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));}
getName() //RLock redissonLock = redisson.getLock(lockKey);
internalLockLeaseTime //internalLockLeaseTime = unit.toMillis(leaseTime);->RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN); //getLockWatchdogTimeout() 默认30s getLockName(threadId)  //final UUID id + threadId

设置锁成功如何执行看门狗机制实现续命

 RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);ttlRemainingFuture.addListener(new FutureListener<Long>() {@Overridepublic void operationComplete(Future<Long> future) throws Exception {if (!future.isSuccess()) {return;}Long ttlRemaining = future.getNow(); //成功是nil 即null// lock acquiredif (ttlRemaining == null) {//成功一定进入的代码块scheduleExpirationRenewal(threadId);}}});

image-20230615153223903

image-20230615160849995

没有获得锁的线程自旋等待,间歇性尝试加锁

image-20230615162117172

getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); //是阻塞等待不会占用资源

ttl时间内如果当前获得锁的线程执行完成了怎么办,订阅模式

image-20230615162952887

订阅的内容将在删除的时候更新

image-20230615163800191

image-20230615163903196

unlock使用了lua代码保证了原子操作


http://www.ppmy.cn/news/402958.html

相关文章

【Android】Room数据库的使用

简介 Room 是在 SQLite 的基础上推出的 Android 库&#xff0c;它是 Google 官方对数据库操作的推荐方式。使用 Room 可以更方便、高效地操作 SQLite 数据库。 使用 添加依赖 在使用 Room 之前&#xff0c;需要在项目中添加 Room 相关的依赖。在 build.gradle 文件中添加以…

Java 小白 重写toString()方法将如下信息输出在控制台上,红色的苹果被称为“糖心富士”,每500克4.98元,买了2500克“糖心富士”,须支付多少钱

class Apple {public String toString(){return "红色的苹果被称为“糖心富士”&#xff0c;每500克4.98元&#xff0c;买了2500克“糖心富士”&#xff0c;须支付多少钱";}public static void main(String[] args){System.out.println(new Apple());} }

二:物理层

一:物理层基本概念 物理层解决如何在连接各种计算机的传输媒体上传输数据比特流&#xff0c;而不是指具体的传输媒体。 物理层主要任务:确定与传输媒体接口有关的一些特性。 二:数据通信模型 通信的目的是传送消息。 两种数据传输方式&#xff1b;

百度网盘的登陆

登陆页面的具体细节还没有完善。 二维码移动&#xff1a; bool isMoveLight;public const int MOVE_STEP 10;private void P1_MouseEnter(object sender, EventArgs e){timer1.Enabled true;isMoveLight true;}private void Timer1_Tick(object sender, EventArgs e){if ((i…

zzulioj 1146: 吃糖果

题目描述 HOHO&#xff0c;终于从Speakless手上赢走了所有的糖果&#xff0c;是Gardon吃糖果时有个特殊的癖好&#xff0c;就是不喜欢连续两次吃一样的糖果&#xff0c;喜欢先吃一颗A种类的糖果&#xff0c;下一次换一种口味&#xff0c;吃一颗B种类的糖果&#xff0c;这样&…

Kotlin学习 - 数据类与单例类

数据类 在Java代码中&#xff0c;数据类通常需要重写equals()、hashCode()、toString()这几个方法。虽然有快捷方式可以自动生成&#xff0c;但是还是要我们去点击生成下&#xff0c;并且一个简单的数据类就算没有其他复杂逻辑看着也挺繁琐的&#xff0c;代码如下&#xff1a;…

青桔文案:销售青桔水果文案

水果店线上营销文案大全&#xff0c;帮助水果店线上运营&#xff0c;优质全面的水果文案&#xff0c;让发文案不再词穷。目前已更新水果文案5800多条&#xff0c;共280多类水果。 1、怎么找到这些文案 如果想要这些文案&#xff0c;在朋友圈下有个搜一搜功能&#xff0c;直接搜…

Java | ThreadLocal学习笔记

ThreadLocal&#xff0c;它可以在一个线程中传递同一个对象 学习链接&#xff1a;使用ThreadLocal - 廖雪峰的官方网站 eg&#xff1a; public class ThreadLocalDemo {public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(new Stu…