使用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解决发多张券的并发问题的更多相关文章
- Lua + Redis 解决高并发
一.业务背景 优惠券业务主要提供用户领券和消券的功能:领取优惠券的动作由用户直接发起,由于资源有限,我们必须对用户的领取动作进行一些常规约束. 约束1(优惠券维度): 券的最大数量 max: 约束2( ...
- 基于nginx+lua+redis高性能api应用实践
基于nginx+lua+redis高性能api应用实践 前言 比较传统的服务端程序(PHP.FAST CGI等),大多都是通过每产生一个请求,都会有一个进程与之相对应,请求处理完毕后相关进程自动释放. ...
- 基于Lua脚本解决实时数据处理流程中的关键问题
摘要 在处理实时数据的过程中需要缓存的参与,由于在更新实时数据时并发处理的特点,因此在更新实时数据时经常产生新老数据相互覆盖的情况,针对这个情况调查了Redis事务和Lua脚本后,发现Redis事务并 ...
- 豌豆夹Redis解决方式Codis源代码剖析:Proxy代理
豌豆夹Redis解决方式Codis源代码剖析:Proxy代理 1.预备知识 1.1 Codis Codis就不详细说了,摘抄一下GitHub上的一些项目描写叙述: Codis is a proxy b ...
- 【网页加速】lua redis的二次升级
之前发过openresty的相关文章,也是用于加速网页速度的,但是上次没有优化好代码,这次整理了下,优化了nginx的配置和lua的代码,感兴趣的话可以看看上篇的文章: https://www.cnb ...
- 【重要】Nginx模块Lua-Nginx-Module学习笔记(三)Nginx + Lua + Redis 已安装成功(非openresty 方式安装)
源码地址:https://github.com/Tinywan/Lua-Nginx-Redis 一. 目标 使用Redis做分布式缓存:使用lua API来访问redis缓存:使用nginx向客户端提 ...
- Gulp解决发布线上文件(CSS和JS)缓存问题
Gulp解决发布线上文件(CSS和JS)缓存问题 本文的缘由:目前经常线上发布文件后要不断的刷新页面及过很长时间,页面上的CSS和JS文件才能生效,特别对于目前做微信商城的时候,微信内置的浏览器缓存非 ...
- Nginx+Lua+Redis 对请求进行限制
Nginx+Lua+Redis 对请求进行限制 一.概述 需求:所有访问/myapi/**的请求必须是POST请求,而且根据请求参数过滤不符合规则的非法请求(黑名单), 这些请求一律不转发到后端服务器 ...
- Nginx与Redis解决高并发问题
原文链接:http://bbs.phpchina.com/forum.php?mod=viewthread&tid=229629 第一版产品采用的是Jquery,Nginx,PHP(CI框架) ...
随机推荐
- RedHat-Linux操作指令第1篇
不同的linux系统切换方式会稍有一点差别 从图形界面切换到字符界面:Alt+F(1-8) 或者 Alt+Ctrl+Shift+F(1-8) 从字符界面切换回图形界面:Alt+F7 字符界面启动到图形 ...
- 再次学习sql注入
爆所有数据库 select schema_name from information_schema.schemata 先爆出多少个字段 id = 1 order by ?; mysql5.0及以上 都 ...
- 【转载—“光荣之路”公众号】Bug预防体系(上千bug分析后总结的最佳实践)
web常见产品问题及预防 测试人员在每次版本迭代中,会对项目的整体质量有一个把控,对于项目常见的问题,开发经常犯的错误都会有所了解,为了避免或者减少这样的错误或不规范的事情再发生,测试人员可以整理构建 ...
- postman学习网址
postman使用详解: http://gold.xitu.io/entry/57597a62a341310061337885 https://www.getpostman.com/docs/writ ...
- golang GMP goroutine调度器
Goroutine可以动态的伸缩栈的大小,最小2-4kb,最大1GB
- 什么是VIP?什么是IP漂移?
IP地址和MAC地址 在 TCP/IP 的架构下,所有想上网的电脑,不论是用何种方式连上网路,都必须要有一个唯一的 IP-address.事实上IP地址是主机硬件地址的一种抽象,简单的说,MAC地址是 ...
- [原创] C# 金额大写
突然要用到这个功能.也网上找了下. 最后还是自动动手写了一个. 估计这个还是有人要要的,所以顺便发出来吧. 引用保留 https://www.cnblogs.com/goldli/p/14105832 ...
- Panda Global 要点聚焦,区块链在数字医疗的落地应
据Panda Global,随着区块链技术影响力的不断扩大,其应用性已涉及更加广泛的领域,不断更新着人们的认知.在区块链技术未介入之前,关于医疗行业和数字经济结合早已不是什么新鲜话题,相关研究不少 但 ...
- nginx优化-转载
(1)nginx运行工作进程个数,一般设置cpu的核心或者核心数x2 如果不了解cpu的核数,可以top命令之后按1看出来,也可以查看/proc/cpuinfo文件 grep ^processor / ...
- 剑指Java高效编程教程
教程介绍 所谓"武以快为尊,天下武功唯快不破".本课程剑指Java高效编程,致力于从"技术"和"工具"两大 维度提高编程效率,帮助广大程序员 ...