简述

有一些场景我们可以在一段代码中多次操作redis,每次请求Redis都要去Jedis/Lettuce连接池申请一个连接请求一次redis服务进行缓存操作。

这样不仅有网络的消耗,假如在redis连接数吃紧的情况下多次请求redis很有可能回造成redis获取连接超时。懂得redis的兄弟这时候会说可以使用pipeline解决啊!确实我们使用pipeline批量命令去请求redis会解决上述问题,但还有一种场景就是我上一个redis请求成功了,但下一个请求失败了,这个时候上一个请求我需要回滚怎么办?

那这个时候我们就需要使用lua脚本做事务请求。在lua脚本中如果说有一个命令失败,整个脚本都会执行失败。也就是像我们数据库(ACID)一样可以支持事务的执行,让整段redis的命令具有原子性的。

当然我们还可以使用TCC,对每一步redis的命令操作进行try catch。如果出现异常就再次执行命令将之前修改的缓存状态修改回来。

Lua脚本使用

Java 代码

public int executeLuaScript () {// Redis脚本对象DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();//设置返回值类型redisScript.setResultType(Long.class); // 设置返回类型// 这里处理传resource也可以传文本,一般情况下建议传一个constant string进来,不需要每次走IO读取文件,也可以将resource缓存q起来// redisScript.setScriptText("xxxx");redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("RedisTest.lua")));// 设置lua脚本文件路径List<Object> keys = new ArrayList<>(); // lua脚本中所有的KEYkeys.add("TEST-LUA1");// 缓存类型根据具体情况而定redisTemplate.opsForValue().set("TEST-LUA1", 0);Long executeObj = redisTemplate.opsForValue().getOperations().execute(redisScript, keys, 60, "1");System.out.println(executeObj);assert executeObj != null;return executeObj.intValue();
}

lua脚本 RedisTest.lua

local num = redis.call('incr', KEYS[1])
if tonumber(num) == 1 thenredis.call('expire', KEYS[1], ARGV[1])return 1
elseif tonumber(num) > tonumber(ARGV[2]) thenreturn 0
elsereturn 1
end
  • lua脚本中keys[1]、keys[2]、keys[3]对应keys中的元素keys.get(0) / keys.get(2)…
  • lua 脚本中使用redis.call 调用redis命令, 第一个参数是redis命令名称,第二个参数是键,第三个参数(可选参数)是值。
  • tonumber 是lua 函数,我们可以使用lua语法和库来控制脚本逻辑和计算。

简单的lua语法介绍

// 定义变量
local strings val = "world"// 打印
print(val)// 定义数组
local tables myArray = {"redis", "jedis", true, 88.0}
print(myArray[3])// for
local int sum = 0
for i = 1, 100
dosum = sum + i
end
print(sum) -- 5050for index, value in ipairs(myArray)
doprint(index)print(value)
end// while
local int sum = 0
local int i = 0
while i <= 100
dosum = sum + ii = i + 1
end
print(sum) -- 5050// if else
if myArray[i] == "jedis" thenprint(true)
else -- do nothing
end// 定义函数
funtion funcName ()...
end

更多lua语法请访问 http://www.lua.org 进行学习

应用场景介绍

有这样一个场景,一个场活动只能有100个人报名,超过100个人就不能报名了,后面报名的人直接返回失败。有3台服务,nginx轮询负载到这三台服务。对于这个场景我们有2种通用的解决方案:

① Redis限流

利用redis进行限流操作,每次进入代码块先incr一次报名人数缓存,然后判断报名的人数是否超过了100次,如果超过了就直接返回报名人数已满。如果没有超过100次则进行相应的业务处理。这样做事非阻塞的,而且不会出现超出报名次数的问题。

存在的问题:

  • 如果是集群的话,多个命令可能会落到多个节点上,这个时候lua脚本就不能保证是原子性的。这个时候可以利用hashtag来让所有的报名用到的缓存落到同一台服务上即可。

  • 但这样是强依赖redis的,如果说redis挂掉了(单节点),报名服务就不可用了。如果是redis集群部署的话,从节点选举为主节点的时候丢失了数据,本来报名人数是100,现在从节点只同步到99。当然这种情况的发生概率可以忽略不计了。

@Test
public void testSetCacheCount () throws InterruptedException {long start = System.currentTimeMillis();ExecutorService executorService = Executors.newFixedThreadPool(50);redisTemplate.delete(COUNT_KEY);Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(COUNT_KEY, 0);if (!aBoolean) {throw new RuntimeException("Redis 不可用...");} else {CountDownLatch countDownLatch = new CountDownLatch(10000);for (int i = 0; i < 10000; i++) {executorService.execute(() -> {try {Long increment = redisTemplate.opsForValue().increment(COUNT_KEY);if (increment <= LIMIT_COUNT) {// lua 脚本操作(伪代码)atomicLong.incrementAndGet();}countDownLatch.countDown();} catch (Exception e) {// 这里请求失败大多都是redis获取不到连接超时了System.out.println("错误发生了:" + e.getMessage());}});}countDownLatch.await();}int currentCount = Integer.parseInt(String.valueOf(redisTemplate.opsForValue().get(COUNT_KEY)));System.out.println(currentCount);System.out.println("执行了业务代码:" + atomicLong + "次");long end = System.currentTimeMillis();System.out.println("Cost time is:" + (start - end));executorService.shutdown();// 不加锁 357// 加锁 747
}public static void inc (RedisTemplate redisTemplate, int LIMIT_COUNT, AtomicLong atomicLong, String COUNT_KEY) {Long increment = redisTemplate.opsForValue().increment(COUNT_KEY);if (increment <= LIMIT_COUNT) {// 这段可能要修改多个缓存atomicLong.incrementAndGet(); // 代码块 }
}

模拟10000 个请求,50个线程进行处理,大概357秒左右。不管我请求数有多少、失败的请求有多少(这里请求失败主要是因为获取不到连接,失败额的话直接熔断)最终请求进入到atomicLong.incrementAndGet() 只会有100个人。如果我们人数增加了之后执行下面代码失败了,但这个时候报名限制的人数COUNT_KEY已经增加上去了。这个时候我们就可以使用lua脚本来处理整段redis操作让其变成事务处理。

RedisLimit.lua

local num = redis.call('incr', KEYS[1])
if tonumber(num) == 1 thenredis.call('expire', KEYS[1], ARGV[1])return 1
elseif tonumber(num) > tonumber(ARGV[2]) thenreturn 0
elsereturn 1
end

业务代码

Long increment = redisTemplate.opsForValue().increment(COUNT_KEY);
if (increment <= LIMIT_COUNT) {// lua 脚本操作(伪代码)atomicLong.incrementAndGet();
}// 上面代码改为
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//设置返回值类型
redisScript.setResultType(Long.class);
// redis
// redisScript.setScriptText("xxxx");
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("RedisLimit.lua")));//设置lua脚本文件路径
List<Object> keys = new ArrayList<>();
keys.add(COUNT_KEY);
keys.add(LIMIT_KEY);Long execute = redisTemplate.opsForValue().getOperations().execute(redisScript, keys, "60", "1");if (execute == 1) {// 成功
} else {// 失败
}

② 分布式锁

用分布式锁解决的话也就是要每次同一时间只能有一个请求进入到报名处理的代码中对报名人数进行加减。这样虽然没有什么问题但性能势必会很低。对于每个服务来说扣除库存都是阻塞的,而且获取分布式锁之后可能伴随着大量的业务处理逻辑进一步阻塞减慢集群的处理效率,这种场景不推荐使用此种方式。

Springboot 中的Redis 事务使用相关推荐

  1. SpringBoot中使用redis事务

    本文基于SpringBoot 2.X 事务在关系型数据库的开发中经常用到,其实非关系型数据库,比如redis也有对事务的支持,本文主要探讨在SpringBoot中如何使用redis事务. 事务的相关介 ...

  2. springboot中使用redis详解

    一.redis简介 redis是一款高性能key-value(键值对)内存型数据库,是非关系型数据库的一种,它采用单线程的架构方式,避免了多线程存在的锁处理造成的资源耗费,读取速度非常快,非常适合变化 ...

  3. Docker中搭建redis分片集群,搭建redis哨兵结构,实现springboot中对redis分片集群、哨兵结构的访问,Redis缓存雪崩、缓存击穿处理(非关系型数据库技术课程 第十二周)

    文章目录 一.要求: 二.知识总结 缓存雪崩 解决方案 docker中redis分片集群搭建 配置好配置文件 redis-6380.conf redis-6381.conf redis-6382.co ...

  4. 你知道如何在springboot中使用redis吗

    特别说明:本文针对的是新版 spring boot 2.1.3,其 spring data 依赖为 spring-boot-starter-data-redis,且其默认连接池为 lettuce ​  ...

  5. SpringBoot中集成Redis实现对redis中数据的解析和存储

    场景 SpringBoot中操作spring redis的工具类: SpringBoot中操作spring redis的工具类_霸道流氓气质的博客-CSDN博客 上面讲的操作redis的工具类,但是对 ...

  6. SpringBoot中使用Redis保存对象或集合

    1,引入SpringBoot中Redis依赖 <!-- redis --> <dependency><groupId>org.springframework.boo ...

  7. redis:01入门指南以及在springboot中使用redis

    https://redis.io/download step1:参考官网的安装很简单 wget http://download.redis.io/releases/redis-5.0.6.tar.gz ...

  8. SpringBoot中Service层事务控制

    SpringBoot中使用事务比较简单,在Application启动类上添加@EnableTransactionManagement注解,然后在service层的方法上添加@Transactional ...

  9. java中关闭redis事务_Redis 事务支持

    原标题:Redis 事务支持 Redis 事务支持 Redis中事务相关的命令有MULTI.EXEC.DISCARD.WATCH和UNWATCH. Redis事务保证原子性:要么所有命令都执行(都执行 ...

最新文章

  1. iOS三种拨打电话的方法
  2. flume ng之组件介绍
  3. Android多个imei如何获取,如何在Android 10中获取IMEI号,这是获取在Android 10及以下Android 10中获取IMEI号的代码...
  4. python雷达和柱形图_Python Pygal常见数据图(折线图、柱状图、饼图、点图、仪表图和雷达图)详解...
  5. boost::graph模块实现在无向图上使用连通分量算法
  6. 现代软件工程讲义 3 代码规范与代码复审
  7. C语言中临时变量写在哪里,C语言中不允许创建临时变量,交换两个数的内容
  8. python 0xa什么意思_python使用xpath中遇到:Element a at 0x39a9a80到底是什么?
  9. (转载)BitCometTracker使用指南
  10. 【邮件处理】邮件eml文件解析
  11. Win10邮箱管理QQ邮箱+163邮箱
  12. WhatsApp 批量解封提交工具
  13. Python使用selenium自动打开谷歌浏览器和网页
  14. python 课程学习
  15. 【日常】FAB法则在产品设计的应用
  16. RTSP 和 RTMP原理 通过ffmpeg实现将本地摄像头推流到RTSP服务器
  17. 在不解压缩的情况下修改压缩包内的文件
  18. 在伦敦金中学画趋势线
  19. Ruby基础入门学习总结
  20. 并行NFS: 打破NFS 的性能瓶颈

热门文章

  1. 网络协议 终章 - GTP 协议:复杂的移动网络 1
  2. 就业指导——自我介绍与个人简历
  3. 短信链接可以直接跳转微信小商店么?
  4. 知识图谱06:知识图谱的表示思维导图
  5. UBTC项目9月份最新进展
  6. Java中调用C++代码
  7. java cucumber_行为驱动:Cucumber + Java - 实现数据的参数化
  8. Android Toast 自定义显示时长
  9. Python基础题目解析
  10. mimikatz 使用