需求
用户在平台注册时,需要申请一个sip号,由固定部分 + 变化部分
组成,其中变化的部分是从1000001
开始,逐渐递增的,每申请一个,则加1。采用微服务的部署方式,能够从微服务集群中不冲突的获取号码。
方案设计
- 在单个服务内,通过synchronized防止多个线程抢一个资源,实现数据的一致性
- 在微服务集群中,通过分布式锁,防止多个服务抢同一个资源,实现分布式服务数据的一致性
- 这里采用redis分布式锁实现,实现如下:
- SETNX key val:仅当key不存在时,设置一个 key 为 value 的字符串,返回1;若 key 存在,设置失败,返回 0;
- Expire key timeout:为 key 设置一个超时时间,以 second 秒为单位,超过这个时间锁会自动释放,避免死锁;
- DEL key:删除 key。
代码实现
private static final String NUM_LOCK_KEY = "NUM_LOCK";
private static final Long NUM_LOCK_EXPIRE = 500;
private static final Long MAX_NUMBER_INDEX = 1;
// 获取一个当前可用号码
private synchronized String getMaxValue() {
// 获取号码锁,防止并发导致数据异常
String lockKey = NUM_LOCK_KEY;
RedisAtomicLong counter = new RedisAtomicLong(lockKey, redis.getConnectionFactory());
Long lockValue = counter.incrementAndGet();
// 如果其他线程调用,则等待重试,直到5次结束
int count = 0;
while (lockValue > 0 && count < 5) {
try {
Thread.sleep(NUM_LOCK_EXPIRE);
} catch (InterruptedException e) {
log.error("线程休眠异常, {}", e);
}
lockValue = counter.incrementAndGet();
count++;
}
if (lockValue > 0) {
log.info("获取号码锁失败");
return null;
} else {
// 获得锁的线程设置锁的有效期,防止永久上锁
redis.expire(lockKey, configValue.getSipNumLockExpire(), TimeUnit.MILLISECONDS);
}
Map<String, Object> queryMap = new HashMap<>();
queryMap.put("id", MAX_NUMBER_INDEX);
MaxNo maxNo = maxNoDao.selectOneByMap(queryMap);
long result = maxNoDao.update(maxNo);
if (result == 0) {
log.info("更新号码失败");
return null;
}
// 释放号码锁
redis.delete(lockKey);
return maxNo.getMaxValue().toString();
}