前言
公司有一个发券的接口有并发安全问题,下面列出这个问题和解决这个问题的方式。
业务描述
这个接口的作用是给会员发多张券码。涉及到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多张券的并发内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!