使用lua+redis解决发多张券的并发问题

前言

公司有一个发券的接口有并发安全问题,下面列出这个问题和解决这个问题的方式。

业务描述

这个接口的作用是给会员发多张券码。涉及到4张主体,分别是:用户,券,券码,用户领取记录。
下面是改造前的伪代码。
主要是因为查出券码那行存在并发安全问题,多个线程拿到同几个券码。以下都是基于如何让取券码变成原子的去展开。

public boolean sendCoupons(Long userId, Long couponId) {
 // 一堆校验
 // ...
 // 查出券码
 List<CouponCode> couponCodes = couponCodeService.findByCouponId(couponId, num);
 // batchUpdateStatus是一个被@Transactional(propagation = Propagation.REQUIRES_NEW)修饰的方法
 // 批量更新为已被领取状态
 couponCodeService.batchUpdateStatus(couponCods);
 // 发券
 // 发权益
 // 新增用户券码领取记录
}

改造过程

因为券码是多张,想用lua+redis的list结构去做弹出。为什么用这种方案是因为for update直接被否了。

这是写的lua脚本。。

local result = {}
for i=1,ARGV[1],1 do
 result[i] = redis.call("lpop", KEYS[1])
end
return table.contact(result , "|")

这是写的执行lua脚本的client。。其实主要的解决方法就是在redis的list里rpush(存),lpop(取)取数据

@Slf4j
@Component
public class CouponCodeRedisQueueClient implements InitializingBean {

 /**
  * redis lua脚本文件路径
  */
 public static final String POP_COUPON_CODE_LUA_PATH = "lua/pop-coupon-code.lua";
 public static final String SEPARATOR = "|";

 private static final String COUPON_CODE_KEY_PATTERN = "PROMOTION:COUPON_CODE_{0}";
 private String LUA_COUPON_CODE_SCRIPT;

 private String LUA_COUPON_CODE_SCRIPT_SHA;

 @Autowired
 private JedisTemplate jedisTemplate;

 @Override
 public void afterPropertiesSet() throws Exception {

  LUA_COUPON_CODE_SCRIPT = Resources.toString(Resources.getResource(POP_COUPON_CODE_LUA_PATH), Charsets.UTF_8);
  if (StringUtils.isNotBlank(LUA_COUPON_CODE_SCRIPT)) {

   LUA_COUPON_CODE_SCRIPT_SHA = jedisTemplate.execute(jedis -> {
    return jedis.scriptLoad(LUA_COUPON_CODE_SCRIPT);
   });
   log.info("redis lock script sha:{}", LUA_COUPON_CODE_SCRIPT_SHA);
  }

 }

 /**
  * 获取Code
  *
  * @param activityId
  * @param num
  * @return
  */
 public List<String> popCouponCode(Long activityId, String num , int retryNum) {
  if(retryNum == 0){
   log.error("reload lua script error , try limit times ,activityId:{}", activityId);
   return Collections.emptyList();
  }
  List<String> keys = Lists.newArrayList();
  String key = buildKey(String.valueOf(activityId));
  keys.add(key);
  List<String> args = Lists.newArrayList();
  args.add(num);

  try {
   Object result = jedisTemplate.execute(jedis -> {
    if (StringUtils.isNotBlank(LUA_COUPON_CODE_SCRIPT_SHA)) {
     return jedis.evalsha(LUA_COUPON_CODE_SCRIPT_SHA, keys, args);
    } else {
     return jedis.eval(LUA_COUPON_CODE_SCRIPT, keys, args);
    }
   });
   log.info("pop coupon code by lua script.result:{}", result);
   if (Objects.isNull(result)) {
    return Collections.emptyList();
   }
   return Splitter.on(SEPARATOR).splitToList(result.toString());
  } catch (JedisNoScriptException jnse) {
   log.error("no lua lock script found.try to reload it", jnse);
   reloadLuaScript();
   //加载后重新执行
   popCouponCode(activityId, num, --retryNum);
  } catch (Exception e) {
   log.error("failed to get a redis lock.key:{}", key, e);
  }
  return Collections.emptyList();
 }

 /**
  * 重新加载LUA脚本
  *
  * @throws Exception
  */
 public void reloadLuaScript() {
  synchronized (CouponCodeRedisQueueClient.class) {
   try {
    afterPropertiesSet();
   } catch (Exception e) {
    log.error("failed to reload redis lock lua script.retry load it.");
    reloadLuaScript();
   }
  }
 }

 /**
  * 构建Key
  *
  * @param activityId
  * @return
  */
 public String buildKey(String activityId) {
  return MessageFormat.format(COUPON_CODE_KEY_PATTERN, activityId);
 }

}

当然这种操作需要去提前把所有券的券码丢到redis里去,这里我们也碰到了一些问题(券码量比较大的情况下)。比如开始直接粗暴的用@PostConstruct去放入redis,导致项目启动需要很久很久。。这里就不展开了,说一下我们尝试的几种方法

  • @PostConstruct注解
  • CommandLineRunner接口
  • redis的pipeline技术
  • 先保证每个卡券有一定量的券码在redis,再用定时任务定时(根据业务量)去补

到此这篇关于使用lua+redis解决发多张券的并发问题的文章就介绍到这了,更多相关redis多张券的并发内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Redis实现高并发计数器

    业务需求中经常有需要用到计数器的场景:譬如一个手机号一天限制发送5条短信.一个接口一分钟限制多少请求.一个接口一天限制调用多少次等等.使用Redis的Incr自增命令可以轻松实现以上需求.以一个接口一天限制调用次数为例: /** * 是否拒绝服务 * @return */ private boolean denialOfService(String userId){ long count=JedisUtil.setIncr(DateUtil.getDate()+"&"+user

  • 如何利用Redis锁解决高并发问题详解

    redis技术的使用: redis真的是一个很好的技术,它可以很好的在一定程度上解决网站一瞬间的并发量,例如商品抢购秒杀等活动... redis之所以能解决高并发的原因是它可以直接访问内存,而以往我们用的是数据库(硬盘),提高了访问效率,解决了数据库服务器压力. 为什么redis的地位越来越高,我们为何不选择memcache,这是因为memcache只能存储字符串,而redis存储类型很丰富(例如有字符串.LIST.SET等),memcache每个值最大只能存储1M,存储资源非常有限,十分消耗内

  • Nginx+Lua+Redis构建高并发Web应用

    本文介绍如何用Nginx+Lua+Redis来构建高并发Web应用,Curl请求Nginx,Nginx通过Lua查询Redis,返回json数据. 一.安装1.安装lua-redis-parser 复制代码 代码如下: #git clone https://github.com/agentzh/lua-redis-parser.git #export LUA_INCLUDE_DIR=/usr/include/lua5.1 #make CC=gcc #make install CC=gcc 2.安

  • Redis瞬时高并发秒杀方案总结

    1.Redis 丰富的数据结构(Data Structures) 字符串(String) Redis字符串能包含任意类型的数据;: 一个字符串类型的值最多能存储512M字节的内容: 利用INCR命令簇(INCR, DECR, INCRBY)来把字符串当作原子计数器使用: 使用APPEND命令在字符串后添加内容. 列表(List) Redis列表是简单的字符串列表,按照插入顺序排序: 你可以添加一个元素到列表的头部(左边:LPUSH)或者尾部(右边:RPUSH): 一个列表最多可以包含232-1个

  • PHP实现Redis单据锁以及防止并发重复写入

    一.写在前面: 在整个供应链系统中,会有很多种单据(采购单.入库单.到货单.运单等等),在涉及写单据数据的接口时(增删改操作),即使前端做了相关限制,还是有可能因为网络或异常操作产生并发重复调用的情况,导致对相同单据做相同的处理: 为了防止这种情况对系统造成异常影响,我们通过Redis实现了一个简单的单据锁,每个请求需先获取锁才能执行业务逻辑,执行结束后才会释放锁:保证了同一单据的并发重复操作请求只有一个请求可以获取到锁(依赖Redis的单线程),是一种悲观锁的设计: 注:Redis锁在我们的系

  • jedispool连redis高并发卡死的问题

    java端在使用jedispool 连接redis的时候,在高并发的时候经常死锁,或报连接异常,JedisConnectionException,或者getResource 异常等各种问题 在使用jedispool 的时候一定要注意两点 1. 在获取 jedisPool和jedis的时候加上线程同步,保证不要创建过多的jedispool 和 jedis 2. 用完Jedis实例后需要返还给JedisPool 整理了一下redis工具类,通过大量测试和高并发测试的 package com.casp

  • 详解利用redis + lua解决抢红包高并发的问题

    抢红包的需求分析 抢红包的场景有点像秒杀,但是要比秒杀简单点. 因为秒杀通常要和库存相关.而抢红包则可以允许有些红包没有被抢到,因为发红包的人不会有损失,没抢完的钱再退回给发红包的人即可. 另外像小米这样的抢购也要比淘宝的要简单,也是因为像小米这样是一个公司的,如果有少量没有抢到,则下次再抢,人工修复下数据是很简单的事.而像淘宝这么多商品,要是每一个都存在着修复数据的风险,那如果出故障了则很麻烦. 基于redis的抢红包方案 下面介绍一种基于Redis的抢红包方案. 把原始的红包称为大红包,拆分

  • 使用lua+redis解决发多张券的并发问题

    前言 公司有一个发券的接口有并发安全问题,下面列出这个问题和解决这个问题的方式. 业务描述 这个接口的作用是给会员发多张券码.涉及到4张主体,分别是:用户,券,券码,用户领取记录. 下面是改造前的伪代码. 主要是因为查出券码那行存在并发安全问题,多个线程拿到同几个券码.以下都是基于如何让取券码变成原子的去展开. public boolean sendCoupons(Long userId, Long couponId) { // 一堆校验 // ... // 查出券码 List<CouponCo

  • Redis解决库存超卖问题实例讲解

    商品和订单服务间使用MQ 商品服务的库存变化时,通过 MQ 通知订单服务库存变化. 原始的同步流程 查询商品信息 (调用商品服务) 计算总价(生成订单详情) 商品服务扣库存(调用商品服务) 订单入库( 生成订单) // 原始的MySQL同步流程 // 判断此代金券是否加入抢购 SeckillVouchers seckillVouchers = seckillVouchersMapper.selectVoucher(voucherId); AssertUtil.isTrue(seckillVouc

  • Redis解决Session共享问题的方法详解

    企业项目中,一般都是将项目部署到多台服务器上,用nginx做负载均衡.这样可以减轻单台服务器的压力,不过这样也带来一些问题,例如之前单机部署的话,session存取都是直接了当的,因为请求就只到这一台服务器上,不需要考虑数据共享.接下来分别用8000和8001端口启动同一个项目,做一个简单演示: 测试接口代码: package com.wl.standard.controller; import cn.hutool.core.util.StrUtil; import com.wl.standar

  • Nginx利用Lua+Redis实现动态封禁IP的方法

    一.背景 我们在日常维护网站中,经常会遇到这样一个需求,为了封禁某些爬虫或者恶意用户对服务器的请求,我们需要建立一个动态的 IP 黑名单.对于黑名单之内的 IP ,拒绝提供服务. 本文给大家介绍的是Nginx利用Lua+Redis实现动态封禁IP的方法,下面话不多说了,来一起看看详细的介绍吧 二.架构 实现 IP 黑名单的功能有很多途径: 1.在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求: 2.在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者

  • redis 解决key的乱码问题,并清理详解

    key乱码问题 因redis默认使用JdkSerializationRedisSerializer来进行序列化,造成key是乱码,如下: keys '*!report:flag:phon*' 1) "\xac\xed\x00\x05t\x00!report:flag:phone_156464" 2) "\xac\xed\x00\x05t\x00!report:flag:phone_198946" 3) "\xac\xed\x00\x05t\x00!repo

  • Redis中的String类型及使用Redis解决订单秒杀超卖问题

    本系列将和大家分享Redis分布式缓存,本章主要简单介绍下Redis中的String类型,以及如何使用Redis解决订单秒杀超卖问题. Redis中5种数据结构之String类型:key-value的缓存,支持过期,value不超过512M. Redis是单线程的,比如SetAll & AppendToValue & GetValues & GetAndSetValue & IncrementValue & IncrementValueBy等等,这些看上去像是组合命

  • redis 解决库存并发问题实现数量控制

    目录 一.命令 二.常见场景 三.流程图与代码 redis是单进程,阻塞式,在同一时刻只能处理一个请求,后来的请求需要排队等待. 优点:因为是单进程,所以无需处理并发问题,降低 系统复杂度 缺点:不适合缓存大尺寸对象(超过100kb) 原因: 由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高. 而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis也在存储大数据的性能上进行了优化,但是比起

  • Unable to connect to Redis无法连接到Redis解决的全过程

    目录 项目场景: 完整的项目报错信息: 问题描述: 原因分析: 解决方案: 总结 项目场景: 提示:这里简述项目相关背景: 在某个项目中的提交按钮不好用 org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect t

  • 利用nginx+lua+redis实现反向代理方法教程

    前言 最近因为工作需要,要进行IVR的重构, 我们现在系统接了三家IVR服务商, N个业务, 由于IVR这玩意一般只能外网回调, 而开发环境又不允许外网随便访问, 着实烦人. 所有我们打算重构一把, 封装多家IVR, 对业务透明, 同时回调可以针对多家IVR服务商的不同callid直接转发到当时请求的同学的 开发域名去. 而不同的IVR服务商的callid参数是不同的,有的是在url里面(call_id), 有的则是直接post的json数据(callid), 所以太扯了. 直接用lua处理下,

随机推荐