其他
优惠券超发事故:扣了我3个月绩效...
上一篇:分布式系统设计模式,你用过哪些?
大家好,我是顶级架构师。
问题抛出
问题引发
问题解决
问题抛出
<update id="reduceStock">
update coupon set stock = stock - 1 where id = #{coupon_id}
</update>
问题引发
问题解决
| 解决方案 1(Java 代码加锁)
LoginUser loginUser = LoginInterceptor.threadLocal.get();
CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>()
.eq("id", couponId)
.eq("category", categoryEnum.name()));
if(couponDO == null){
throw new BizException(BizCodeEnum.COUPON_NO_EXITS);
}
this.checkCoupon(couponDO,loginUser.getId());
//构建领券记录
CouponRecordDO couponRecordDO = new CouponRecordDO();
BeanUtils.copyProperties(couponDO,couponRecordDO);
couponRecordDO.setCreateTime(new Date());
couponRecordDO.setUseState(CouponStateEnum.NEW.name());
couponRecordDO.setUserId(loginUser.getId());
couponRecordDO.setUserName(loginUser.getName());
couponRecordDO.setCouponId(couponDO.getId());
couponRecordDO.setId(null);
int row = couponMapper.reduceStock(couponId);
if(row == 1){
couponRecordMapper.insert(couponRecordDO);
}else{
log.info("发送优惠券失败:{},用户:{}",couponDO,loginUser);
}
}
synchronized 的作用范围是单个 JVM 实例,如果是集群部署系统这里的加锁你可以理解成失效。另外,搜索公众号前端技术精选后台回复“大礼包”,获取一份惊喜礼包。 在使用了 synchronized 加锁后,就会形成串行等待的问题,当一个线程 A 在领取优惠券方法内执行过久时,其它线程会等待直到线程 A 执行结束。
| 解决方案 2(SQL 层面解决超发)
update coupon set stock = stock - 1 where id = #{coupon_id} and stock > 0
</update>
update product set stock=stock-1 where stock=#{上一次的库存} and id = 1 and stock>0
</update>
update product set stock=stock-1,versioin = version+1 where id = 1 and stock>0 and version=#{上一次的版本号}
</update>
| 解决方案 3(通过 Redis 分布式锁来解决问题)
排他性,在分布式集群中,同一个方法,在同一个时间只能被某一台机器上的一个线程执行 容错性,当一个线程上锁后,如果机器突然的宕机,如果不释放锁,此时这条数据将会被锁死 还要注意锁的粒度,锁的开销 满足高可用,高性能,可重入
try{
if(setnx(key,"1")){
//获取到锁
//设置Key的时期时间
exp(key,30,TimeUnit.MILLISECONDS);
try{
//业务逻辑
}finally{
del(key);
}
}else{
//获取锁失败,递归调用这个方法,或者使用for进行自旋获取锁
}
}
String threadId = Thread.currentThread().getId();
try{
if(setnx(key,threadId)){
//获取到锁
//设置Key的时期时间
exp(key,30,TimeUnit.MILLISECONDS);
try{
//业务逻辑
}finally{
if(get(key) == threadId){
del(key);
}
}
}else{
//获取锁失败,递归调用这个方法,或者使用for进行自旋获取锁
}
}
redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(key), threadId);
| 解决方案 4(使用 Redis 推荐的方式)
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.4</version>
</dependency>
public class AppConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
@Bean
public RedissonClient redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
return Redisson.create(config);
}
}
String key = "lock:coupon:" + couponId;
RLock rLock = redisson.getLock(key);
LoginUser loginUser = LoginInterceptor.threadLocal.get();
rLock.lock();
try{
//业务逻辑
}finally {
rLock.unlock();
}
return JsonData.buildSuccess();
}
config.setLockWatchdogTimeout();
欢迎大家进行观点的探讨和碰撞,各抒己见。如果你有疑问,也可以找我沟通和交流。
最后给读者整理了一份BAT大厂面试真题,需要的可扫码回复“面试题”即可获取。
「顶级架构师」建立了读者架构师交流群,大家可以添加小编微信进行加群。欢迎有想法、乐于分享的朋友们一起交流学习。
扫描添加好友邀你进架构师群,加我时注明【姓名+公司+职位】
版权申明:内容来源网络,版权归原作者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。
猜你还想看
牛逼!接私活必备的 N 个系统项目!赶快收藏吧(附源码合集第 3 期)!
Nginx + keepalived 实现高可用 + 防盗链 + 动静分离
重试框架 Spring-Retry 和 Guava-Retry,你知道该怎么选吗?