【连载】redis库存操作,分布式锁的四种实现方式[四]--基于Redis lua脚本机制实现分布式锁
一、redis lua介绍
Redis 提供了非常丰富的指令集,但是用户依然不满足,希望可以自定义扩充若干指令来完成一些特定领域的问题。Redis 为这样的用户场景提供了 lua 脚本支持,用户可以向服务器发送 lua 脚本来执行自定义动作,获取脚本的响应数据。Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。
二、高并发情况下减库存的实现思路
由于lua脚本是原子性同步执行的,也就是说,我们可以将一堆操作封装为一个操作,让redis当做一条命令执行,这样,我们在分布式、高并发情况下,做减库存操作,每个客户端在执行操作时,其他客户端都是阻塞状态,相当于变相实现了分布式锁。
1、在本地缓存一份减库存的lua脚本,每次服务启动时,将脚本内容加载至内存;
2、请求处理时,会校验redis-server端是否存在该脚本,若存在,返回脚本的唯一id,客户端根据id调用脚本,并将参数传递过去执行
3、若redis-server端不存在该脚本,会先将脚本发送到server端缓存,返回id,进行调用
三、lua脚本的好处
1、减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延和请求次数。
2、原子性的操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
3、代码复用:客户端发送的脚步会永久存在redis中,这样,其他客户端可以复用这一脚本来完成相同的逻辑。
4、速度快:见 与其它语言的性能比较, 还有一个 JIT编译器可以显著地提高多数任务的性能; 对于那些仍然对性能不满意的人, 可以把关键部分使用C实现, 然后与其集成, 这样还可以享受其它方面的好处。
5、可以移植:只要是有ANSI C 编译器的平台都可以编译,你可以看到它可以在几乎所有的平台上运行:从 Windows 到Linux,同样Mac平台也没问题, 再到移动平台、游戏主机,甚至浏览器也可以完美使用 (翻译成JavaScript).
6、源码小巧:20000行C代码,可以编译进182K的可执行文件,加载快,运行快。
四、代码实现
本地缓存一份减库存的lua脚本
local stockId = KEYS[];
local decrNum = ARGV[];
local result;
print('key为', stockId);
print('value为', decrNum);
local crtStock = redis.call('get', stockId);
print('当前库存为 :', crtStock);
if crtStock == false or crtStock < decrNum then
result = -
else
result = redis.call('decrBy', stockId, decrNum)
end
return result;
服务启动时,将脚本内容加载至内存,由静态字符串DECRBY_STOCK_SCRIPT接收
/**
* 减库存脚本
*/
private static String DECRBY_STOCK_SCRIPT = ""; /**
* 初始化bean后,将加减库存的lua脚本加载至内存中
*/
@PostConstruct
public void loadLuaScript() { InputStream certStream = null;
BufferedReader br = null;
try {
certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lua/decrByStock.lua");
br = new BufferedReader(new InputStreamReader(certStream, "UTF-8"));
StringBuilder luaStr = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
luaStr.append(line).append(" ");
}
DECRBY_STOCK_SCRIPT = luaStr.toString();
LOGGER.info("减库存脚本初始化加载完毕,内容为:" + DECRBY_STOCK_SCRIPT); } catch (Exception e) {
LOGGER.error("初始化库存管理Controller bean,加载操作库存脚本失败!" + e);
} finally {
if (certStream != null) {
try {
certStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
在服务启动时,会打印相应的日志

减库存逻辑代码
/**
* 减库存(基于lua脚本实现)
*
* @param trace 请求流水
* @param stockManageReq(stockId、decrNum)
* @return -1为失败,大于-1的正整数为减后的库存量,-2为库存不足无法减库存
*/
@Override
@ApiOperation(value = "减库存", notes = "减库存")
@RequestMapping(value = "/decrByStock", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public int decrByStock(@RequestHeader(name = "Trace") String trace, @RequestBody StockManageReq stockManageReq) { long startTime = System.currentTimeMillis(); LOGGER.reqPrint(Log.CACHE_SIGN, Log.CACHE_REQUEST, trace, "decrByStock", JSON.toJSONString(stockManageReq)); int res = 0;
String stockId = stockManageReq.getStockId();
Integer decrNum = stockManageReq.getDecrNum(); if (StringUtils.isBlank(DECRBY_STOCK_SCRIPT)) {
LOGGER.error("减库存脚本为空!操作终止");
return -1;
}
LOGGER.info("减库存脚本内容为:" + DECRBY_STOCK_SCRIPT); try {
if (null != stockId && null != decrNum) { stockId = PREFIX + stockId; // 加减库存lua脚本执行
Long result = (Long) this.evalshaScript(stockId, decrNum, DECRBY_STOCK_SCRIPT); LOGGER.info("脚本执行结果,result=" + result); res = result.intValue();
}
} catch (Exception e) {
LOGGER.error(trace, "decr sku stock failure.", e);
res = -1;
} finally {
LOGGER.respPrint(Log.CACHE_SIGN, Log.CACHE_RESPONSE, trace, "decrByStock", System.currentTimeMillis() - startTime, String.valueOf(res));
}
return res;
} /**
* 加减库存lua脚本执行
*
* @param stockId 库存id
* @param changeNum 加减库存的量
* @param script lua脚本
* @return 执行结果
*/
private Object evalshaScript(String stockId, Integer changeNum, String script) { Object result = null;
try (Jedis jedis = jedisPool.getWriteResource()) {
if (jedis.select(0).equals("OK")) {
// 将脚本缓存值redis server端,并返回脚本的唯一标识id
String sha = jedis.scriptLoad(script); // 调用evalsha方法,执行脚本
result = jedis.evalsha(sha, 1, stockId, String.valueOf(changeNum));
}
}
return result;
}
五、ab压测
5W请求,100并发,tps达到了4500,并且没有错误,相当强悍了

六、总结
lua脚本实现,可以保证正确性的同时,完全能够保证数据的一致性,可靠性方面就需要脚本的健壮性来保证,总之,效率比redisson、zk分布式锁要高太多,推荐使用
【连载】redis库存操作,分布式锁的四种实现方式[四]--基于Redis lua脚本机制实现分布式锁的更多相关文章
- 分布式锁的两种实现方式(基于redis和基于zookeeper)
先来说说什么是分布式锁,简单来说,分布式锁就是在分布式并发场景中,能够实现多节点的代码同步的一种机制.从实现角度来看,主要有两种方式:基于redis的方式和基于zookeeper的方式,下面分别简单介 ...
- 【连载】redis库存操作,分布式锁的四种实现方式[三]--基于Redis watch机制实现分布式锁
一.redis的事务介绍 1. Redis保证一个事务中的所有命令要么都执行,要么都不执行.如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行.而一旦客户端发 ...
- 【连载】redis库存操作,分布式锁的四种实现方式[一]--基于zookeeper实现分布式锁
一.背景 在电商系统中,库存的概念一定是有的,例如配一些商品的库存,做商品秒杀活动等,而由于库存操作频繁且要求原子性操作,所以绝大多数电商系统都用Redis来实现库存的加减,最近公司项目做架构升级,以 ...
- 分布式锁的三种实现方式 数据库、redis、zookeeper
版权声明: https://blog.csdn.net/wuzhiwei549/article/details/80692278 一.为什么要使用分布式锁 我们在开发应用的时候,如果需要对某一个共享变 ...
- 【连载】redis库存操作,分布式锁的四种实现方式[二]--基于Redisson实现分布式锁
一.redisson介绍 redisson实现了分布式和可扩展的java数据结构,支持的数据结构有:List, Set, Map, Queue, SortedSet, ConcureentMap, L ...
- 多线程深入:乐观锁与悲观锁以及乐观锁的一种实现方式-CAS(转)
原文:https://www.cnblogs.com/qjjazry/p/6581568.html 首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每 ...
- Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS
首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很 ...
- 乐观锁与悲观锁以及乐观锁的一种实现方式-CAS
首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很 ...
- 关于分布式Session 的几种实现方式
分布式Session的几种实现方式 1.基于数据库的Session共享 2.基于NFS共享文件系统 3.基于memcached 的session,如何保证 memcached 本身的高可用性? 4. ...
随机推荐
- thinkphp中的volist标签
属性: name(必须):要输出的数据模板变量 id(必须):循环变量 offset(可选):要输出数据的offset length(可选):输出数据的长度 key(可选):循环的key变量,默认值为 ...
- Oracle sqlldr数据加载
1 sqlldr 传统路径:sqlldr会利用sql插入为我们加载数据 直接路径加载:sqlldr不适用sql,直接格式化数据块,绕开undo,避开redo,最快的方法就是并行直接路径加载 sqlld ...
- Cassandra学习五 使用Key的正确姿势
NoSQL一般是反范式的,比如提倡数据冗余,使得不至于写出非常复杂的SQL语句. Cassandra之中一共包含下面5中Key: Primary Key: 用来获取某一行的数据,可以是一列或多列 ...
- 安全测试回顾(一)补充:burp 的基本操作
浏览器设置; 拦截请求: Spider 伪造请求: 对这个url 伪造请求 拦截一个页面后,进入攻击模块 sniper 两个参数值 保证一个不变 另一个 进行枚举 battering ram 两个 ...
- Maven的安装及配置、Maven在Eclipse中的配置
一.需要准备的东西 1. JDK 2. Eclipse 3. Maven程序包 二.检查JAVA安装 三.安装Maven 下载apache-maven-3.5.3-bin.zip解压即可. 四.配置M ...
- 让你的javascript代码高大上
1,创造简短的写法 你可以这么写: var slice = Array.prototype.slice; slice.call(arguments); //转化成数组 也可以这么写:(ie9+) va ...
- SpringMVC总结三:请求Controller返回视图类型以及请求方式、参数介绍
视图解析,请求Controller返回的视图类型: @Controller @RequestMapping("/test") public class TestController ...
- 并发之AtomicIntegerArray
5 并发之AtomicIntegerArray 该类是Java对Integer数组支持的原子性操作:在认识这个类之前我们先来看一个方法,这个方法是Integer类中的: public sta ...
- MyBatis—实现关联表查询
一.一对一关联 1.1.提出需求 根据班级id查询班级信息(带老师的信息) 1.2.创建表和数据 创建一张教师表和班级表,这里我们假设一个老师只负责教一个班,那么老师和班级之间的关系就是一种一对一的关 ...
- Jmeter调度器配置
Jmeter的线程组设置里有一个调配器设置,用于设置该线程组下脚本执行的开始时间.结束时间.持续时间及启动延迟时间.当需要半夜执行性能测试时会用到这个功能. ps:设置调度器配置,需要将前面的循环次数 ...