- 一、写在前面
- 二、问题场景
- 三、正确流程
- 四、总结反思
- 五、写在后面
浪漫主义的核心是对现实的憎恶,以及对逃避现实的那种激情洋溢的渴望。
一、写在前面(由于考虑到公司业务和代码安全性问题,所有内容已脱敏)
二、问题场景最近,在实习中遇到如下场景:
还是之前的短信业务,由于生产环境和灰度环境同时发布了定时任务,并且用的同一套数据库,所以,偶尔会出现短信重复发送的问题。即”超卖“现象。
三、正确流程增加分布式锁,使两个环境只有一个定时任务能拿到锁即可。
工具类 JedisLockUtils
@Slf4j
public class JedisLockUtils {
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;
public static boolean tryLock(Jedis jedis, String lockKey, String lockValue, int expireTime) {
try {
String result = jedis.set(lockKey, lockValue, new SetParams().nx().ex(expireTime));
return LOCK_SUCCESS.equals(result);
} finally {
returnResource(jedis);
}
}
public static boolean releaseLock(Jedis jedis, String lockKey, String lockValue) {
try {
String script = "";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
return RELEASE_SUCCESS.equals(result);
} finally {
returnResource(jedis);
}
}
public static void returnResource(final Jedis jedis) {
if (null != jedis) {
jedis.close();
}
}
}
工具锁 RedisLock
@Service
public class RedisLock {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisLock.class);
@Resource
private JedisSentinelPool jedisPool;
public LockResult tryLock(String lockKey, int seconds) {
Jedis jedis = jedisPool.getResource();
long s = new Date().getTime();
String lockId = UUID.randomUUID().toString();
try {
boolean result = false;
int tryCount = 10; // 最多可尝试 10 次获取锁
while (!result && tryCount > 0) {
tryCount--;
result = JedisLockUtils.tryLock(jedis, lockKey, lockId, seconds);
if (result) {
long e = new Date().getTime();
return new LockResult(true, lockId);
}
Thread.sleep(500);
}
} catch (Exception e) {
LOGGER.error("try lock error, lockKey={},lockId={}", lockKey, lockId, e);
}
return new LockResult(false);
}
// 获取不到锁则放弃
public LockResult failOnCantTryLock(String lockKey, int seconds) {
Jedis jedis = jedisPool.getResource();
long s = new Date().getTime();
String lockId = UUID.randomUUID().toString();
try {
boolean result = false;
int tryCount = 10; // 最多可尝试 10 次获取锁
while (!result && tryCount > 0) {
tryCount--;
result = JedisLockUtils.tryLock(jedis, lockKey, lockId, seconds);
if (result) {
long e = new Date().getTime();
return new LockResult(true, lockId);
}
Thread.sleep(500);
}
} catch (Exception e) {
LOGGER.error("try lock error, lockKey={},lockId={}", lockKey, lockId, e);
}
return new LockResult(false);
}
public boolean releaseLock(String lockKey, String lockId) {
Jedis jedis = jedisPool.getResource();
for (int i = 0; i < 3; i++) {
try {
return JedisLockUtils.releaseLock(jedis, lockKey, lockId);
} catch (Exception ignored) {
}
}
return false;
}
public boolean checkLock(String lockKey, String lockId) {
try {
String value = jedisPool.getResource().get(lockKey);
if (StringUtils.equals(lockId, value)) {
return true;
}
} catch (Exception e) {
LOGGER.error("try lock error, lockId={}", lockId, e);
}
return false;
}
}
工具锁 LockResult
@Data
@AllArgsConstructor
@RequiredArgsConstructor
public class LockResult {
@NonNull
private boolean success;
private String lockId;
}
更改后的短信任务
@Component
@Slf4j
public class SmsSendTask {
@Autowired
InviteSmsDOMapper inviteSmsDOMapper;
@Autowired
private SmsUtil smsUtil;
@Autowired
private RedisLock redisLock;
@Scheduled(cron = "0/1 * * * * ?")
public void SmsSendTask() {
LockResult lockResult = null;
try {
lockResult = redisLock.failOnCantTryLock("SmsSendTask", 30);
if (lockResult.isSuccess()) {
}
} catch (Exception e) {
log.error(e.getMessage());
} finally {
if (null != lockResult && lockResult.isSuccess()) {
redisLock.releaseLock("SmsSendTask", lockResult.getLockId());
}
}
}
}
四、总结反思
这个问题其实应该在很早前发现,
生成和灰度抢资源我是没有想到的,而且由于定时任务是每秒除触发,所以刚好行程一个小型的抢单场景。
看来以后写定时任务的时候,要对公司发布流程足够熟悉。
五、写在后面欢迎关注,实现期间会经常记录一些工作中遇到的问题。
欢迎随时留言讨论,与君共勉,知无不答!



