使用lua+redis解決發(fā)多張券的并發(fā)問題
前言
公司有一個發(fā)券的接口有并發(fā)安全問題,下面列出這個問題和解決這個問題的方式。
業(yè)務(wù)描述
這個接口的作用是給會員發(fā)多張券碼。涉及到4張主體,分別是:用戶,券,券碼,用戶領(lǐng)取記錄。
下面是改造前的偽代碼。
主要是因?yàn)椴槌鋈a那行存在并發(fā)安全問題,多個線程拿到同幾個券碼。以下都是基于如何讓取券碼變成原子的去展開。
public boolean sendCoupons(Long userId, Long couponId) {
// 一堆校驗(yàn)
// ...
// 查出券碼
List<CouponCode> couponCodes = couponCodeService.findByCouponId(couponId, num);
// batchUpdateStatus是一個被@Transactional(propagation = Propagation.REQUIRES_NEW)修飾的方法
// 批量更新為已被領(lǐng)取狀態(tài)
couponCodeService.batchUpdateStatus(couponCods);
// 發(fā)券
// 發(fā)權(quán)益
// 新增用戶券碼領(lǐng)取記錄
}
改造過程
因?yàn)槿a是多張,想用lua+redis的list結(jié)構(gòu)去做彈出。為什么用這種方案是因?yàn)閒or update直接被否了。
這是寫的lua腳本。。
local result = {}
for i=1,ARGV[1],1 do
result[i] = redis.call("lpop", KEYS[1])
end
return table.contact(result , "|")
這是寫的執(zhí)行l(wèi)ua腳本的client。。其實(shí)主要的解決方法就是在redis的list里rpush(存),lpop(?。┤?shù)據(jù)
@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();
//加載后重新執(zhí)行
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();
}
}
}
/**
* 構(gòu)建Key
*
* @param activityId
* @return
*/
public String buildKey(String activityId) {
return MessageFormat.format(COUPON_CODE_KEY_PATTERN, activityId);
}
}
當(dāng)然這種操作需要去提前把所有券的券碼丟到redis里去,這里我們也碰到了一些問題(券碼量比較大的情況下)。比如開始直接粗暴的用@PostConstruct去放入redis,導(dǎo)致項(xiàng)目啟動需要很久很久。。這里就不展開了,說一下我們嘗試的幾種方法
- @PostConstruct注解
- CommandLineRunner接口
- redis的pipeline技術(shù)
- 先保證每個卡券有一定量的券碼在redis,再用定時任務(wù)定時(根據(jù)業(yè)務(wù)量)去補(bǔ)
到此這篇關(guān)于使用lua+redis解決發(fā)多張券的并發(fā)問題的文章就介紹到這了,更多相關(guān)redis多張券的并發(fā)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解析Redis未授權(quán)訪問漏洞復(fù)現(xiàn)與利用危害
這篇文章主要介紹了Redis未授權(quán)訪問漏洞復(fù)現(xiàn)與利用,介紹了redis未授權(quán)訪問漏洞的基本概念及漏洞的危害,本文給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-01-01
SpringBoot整合Redis入門之緩存數(shù)據(jù)的方法
Redis是一個開源的使用ANSI C語言編寫、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫,并提供多種語言的API,下面通過本文給大家介紹下SpringBoot整合Redis入門之緩存數(shù)據(jù)的相關(guān)知識,感興趣的朋友一起看看吧2021-11-11
Redis?使用?List?實(shí)現(xiàn)消息隊(duì)列的優(yōu)缺點(diǎn)
這篇文章主要介紹了Redis?使用?List?實(shí)現(xiàn)消息隊(duì)列有哪些利弊,小編結(jié)合消息隊(duì)列的特點(diǎn)一步步帶大家分析使用?Redis?的?List?作為消息隊(duì)列的實(shí)現(xiàn)原理,并分享如何把?SpringBoot?與?Redission?整合運(yùn)用到項(xiàng)目中,需要的朋友可以參考下2022-01-01
RedisDesktopManager?連接redis的方法
這篇文章主要介紹了RedisDesktopManager?連接redis,需要的朋友可以參考下2023-08-08
聊聊使用RedisTemplat實(shí)現(xiàn)簡單的分布式鎖的問題
這篇文章主要介紹了使用RedisTemplat實(shí)現(xiàn)簡單的分布式鎖問題,文中給大家介紹在SpringBootTest中編寫測試模塊的詳細(xì)代碼,需要的朋友可以參考下2021-11-11

