1504—SpringMVC建立SSM整合
阿新 • • 發佈:2021-01-07
前言
公司有一個發券的介面有併發安全問題,下面列出這個問題和解決這個問題的方式。
業務描述
這個介面的作用是給會員發多張券碼。涉及到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,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多張券的併發內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!