需求

  双十二要搞一个一分钱门票抢购的活动。

分析

  性能分析,抢购时会发生高并发,如果仅仅依靠Mysql数据库,有可能因为大量的请求频繁访问数据库造成服务器雪崩,所以考虑通过Redis减库存,最终的数据落地到DB中。

  在高并发的情况下,还要考虑到超卖的问题,因而打算使用Lua脚本完成原子减的操作。

  在这里,我们只针对减库存的操作进行分析。

实现

  不使用原子操作,出现超卖的情况。第一步:先从redis中查出库存进行判断,第二步:如果库存>0,则进行减库存的操作。

  代码实现:

         // 第一步:从redis中查出库存
Integer stock = (Integer) RedisUtils.get("stock"); // 第二步:如果库存>0,则进行减库存的操作
if (stock > 0) {
long spareStock = RedisUtils.decr("stock", 1);
System.out.println(getName() + "抢到了第" + spareStock + "件");
} else {
System.out.println("库存不足");
}

  用多线程模拟并发请求:库存为500,创建505个线程去抢购。

         for(int i =1;i<=505;i++){
MyThread2 thread =new MyThread2("线程"+i);
thread.start();
}

  执行结果:出现超卖问题,原因是:查询库存及减库存不是原子性操作。

  使用原子性操作:直接减库存。

     public void run() {
long stock = RedisUtils.stock("stock");
if (stock > 0) {
System.out.println(getName() + "抢到了第" + stock + "件");
} else {
System.out.println("库存不足");
} }

  Lua脚本实现减库存操作:

 /**
* 库存不足
*/
public static final int LOW_STOCK = 0;
/**
* 不限库存
*/
public static final long UNINITIALIZED_STOCK = -1L; /**
* 执行扣库存的脚本
*/
public static final String STOCK_LUA; static {
// 初始化减库存lua脚本
StringBuilder sb = new StringBuilder();
sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
sb.append(" local stock = tonumber(redis.call('get', KEYS[1]));");
sb.append(" if (stock == -1) then");
sb.append(" return 1;");
sb.append(" end;");
sb.append(" if (stock > 0) then");
sb.append(" redis.call('incrby', KEYS[1], -1);");
sb.append(" return stock;");
sb.append(" end;");
sb.append(" return 0;");
sb.append("end;");
sb.append("return -1;"); STOCK_LUA = sb.toString();
} /**
* 扣库存
*
* @param key 库存key
* @return 扣减之前剩余的库存【0:库存不足; -1:库存未初始化; 大于0:扣减库存之前的剩余库存】
*/
public static Long stock(String key) {
// 脚本里的KEYS参数
List<String> keys = new ArrayList<>();
keys.add(key);
// 脚本里的ARGV参数
List<String> args = new ArrayList<>(); Long result = (Long)redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);
} // 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long)((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
}
return UNINITIALIZED_STOCK;
}
});
return result;
}

  执行结果:505个线程去抢500个商品,有五个线程会抢不到,测试结果与预期一致,解决了超卖的问题。

参考:https://blog.csdn.net/xiaolyuh123/article/details/79208959

使用Lua脚本通过原子减防止超卖的更多相关文章

  1. 记一次项目中解决 -- 并发减库存超卖问题过程(Java)

    起因:项目中要做预约功能,首先每天的余票都是有上限的,自然不能出现超卖的情况 基于我们项目是单体分布式的springcloud部署,我想了下 第一种方法,直接mysql加行锁,要update这条库存数 ...

  2. 基于Lua脚本解决实时数据处理流程中的关键问题

    摘要 在处理实时数据的过程中需要缓存的参与,由于在更新实时数据时并发处理的特点,因此在更新实时数据时经常产生新老数据相互覆盖的情况,针对这个情况调查了Redis事务和Lua脚本后,发现Redis事务并 ...

  3. Redis进阶实践之十九 Redis如何使用lua脚本

    一.引言               redis学了一段时间了,基本的东西都没问题了.从今天开始讲写一些redis和lua脚本的相关的东西,lua这个脚本是一个好东西,可以运行在任何平台上,也可以嵌入 ...

  4. redisTemplate的spring配置以及lua脚本驱动

    最近在使用spring-data-redis的redisTemplate,所以写篇使用记录吧. 1.不用多说,使用maven引入相关依赖,因为项目已经引入其他的 <dependency> ...

  5. Redis Lua脚本原理

    2.6版本之后支持嵌入Lua脚本,客户端使用Lua脚本,直接在服务器端原子的执行多条命令 Lua脚本执行过程 创建并修改Lua环境 1 创建基础Lua环境 2 载入函数库 3 创建全局表格Lua 4 ...

  6. Redis学习笔记六:独立功能之 Lua 脚本

    Redis 2.6 开始支持 Lua 脚本,通过在服务器环境嵌入 Lua 环境,Redis 客户端中可以原子地执行多个 Redis 命令. 使用 eval 命令可以直接对输入的脚本求值: 127.0. ...

  7. Lua脚本之语法基础快速入门

    要 1.基本数据类型 2.Lua中的常用语句结构以及函数 3.Lua中的常用语句结构介绍 4.Lua中的库函数 目录[-] 一.基本数据类型 二.Lua中的常用语句结构以及函数 1.Lua中的常用语句 ...

  8. Lua脚本在redis分布式锁场景的运用

    目录 锁和分布式锁 锁是什么? 为什么需要锁? Java中的锁 分布式锁 redis 如何实现加锁 锁超时 retry redis 如何释放锁 不该释放的锁 通过Lua脚本实现锁释放 用redis做分 ...

  9. Redis结合Lua脚本实现高并发原子性操作

    从 2.6版本 起, Redis 开始支持 Lua 脚本 让开发者自己扩展 Redis … 案例-实现访问频率限制: 实现访问者 $ip 在一定的时间 $time 内只能访问 $limit 次. 非脚 ...

随机推荐

  1. 同级frame之间的通信与跳转

    项目最近需求两个同级的frame,通过点击一个frame里面的btn按钮来实现另一个frame的跳转(注意这俩个frame在同一个frameset下面): 原理:通过该frame找到父frameset ...

  2. 字符串格式连接sqlserver数据库的字段概念解释

    以连接sqlserver数据库举例说明如:“Provider=SQLOLEDB.1;Password=******;Persist Security Info=True;User ID=sa;Init ...

  3. WCF学习笔记(一)---我的第一个WCF程序

    一.创建WCF程序   1.创建一个控制台程序(WCFBlog)   2.添加wcf项目   3.将默认的IService1和Service1改成自己的名字   4.在ICalculateServic ...

  4. JDBC 复习

    概念 Java DataBase Connectivity java数据库连接 定义了操作所有关系型数据库的规则(接口),不同的数据库厂商编写类实现这些接口,这些类就叫数据库驱动,使得用户只需要使用统 ...

  5. Test Title

    test testing... testing in day02... testing in day07...

  6. nginx 配置跨域

    location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, ...

  7. java.lang.UnsatisfiedLinkError: dlopen failed: library "/system/lib/libjni_i

    出错log:java.lang.UnsatisfiedLinkError: dlopen failed: library "/system/lib/libskytvjni.so" ...

  8. SpringBoot 通过配置禁用swagger

    转自:https://blog.csdn.net/weixin_37264997/article/details/82762050 一.序言 在生产环境下,我们需要关闭swagger配置,避免暴露接口 ...

  9. mybatis + oracle,出现ORA-01461:仅能绑定要插入LONG列的LONG值

    1.这个异常是指,用户向数据库执行插入数据操作时,某条数据的某个字段值过长,如果是varchar2类型的,当长度超过2000,--4000(最大值)之间的时候,oracle会自动将该字段值转为long ...

  10. Oracle使用expdp/impdp迁移数据

    Oracle使用expdp/impdp迁移数据 1.#以下步骤在开发库执行(expdp导出) 一.创建逻辑目录,该命令不会在操作系统创建真正的目录,最好以system等管理员创建. sqlplus / ...