在生产上发现有Expected one result (or null) to be returned by selectOne(), but found: 2的报错,后面定位到如下代码,首先进行数据库查询,如果查询不到调百度的ip定位接口,然后再插入到数据库。此处如果有多个线程先进行查询,然后再插入,又因为数据库中IP字段没有设置唯一索引,导致数据库中IP相同的数据会有多条。
@Service
@Transactional
public class AdBaiduLocationServiceImpl implements AdBaiduLocationService{
@Resource(name = "appConfig")
private AppConfig appConfig;
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource(name = "baiduIpLocationDao")
private BaiduIpLocationDao baiduIpLocationDao;
@Override
public AdAreaDto getLocatedAreaByIp(String ip) {
if(StringUtils.isBlank(ip)){
logger.info("【百度接口查询定位】------->缺少必要参数ip");
return null;
}
BaiduIpLocationModel ipDto = baiduIpLocationDao.findByIp(ip);
AdAreaDto adArea = new AdAreaDto();
if(ipDto != null && !StringUtils.isBlank(ipDto.getCity()) && !StringUtils.isBlank(ipDto.getProvince())){
adArea.setProvince(ipDto.getProvince());
adArea.setCity(ipDto.getCity());
return adArea;
}
String locationUrl = appConfig.get("baidu.map.location.ip.url");
String appKey = appConfig.get("baidu.map.location.secretKey");
if(StringUtils.isBlank(locationUrl) || StringUtils.isBlank(appKey)){
logger.error("【百度接口查询定位】------->配置文件baidu.map.location.ip.url和baidu.map.location.secretKey参数缺失。");
return null;
}
Map<String, String> map = new HashMap<String, String>();
map.put("ak", appKey);
map.put("ip", ip);
String responseJson = HttpClientUtils.requestHttpPostMethod(locationUrl, map, appConfig.get("advert.user.adInsideUrl"));
if(StringUtils.isBlank(responseJson)){
throw new BusinessException(new ErrorCode("baidu location","baidu location failed"));
}
JSONObject jsonObject = JSONObject.parseObject(responseJson);
String province = null;
String city = null;
if(jsonObject.containsKey("content")){
JSONObject contentJson = jsonObject.getJSONObject("content");
if(contentJson.containsKey("address_detail")){
JSONObject addressJson = contentJson.getJSONObject("address_detail");
if(addressJson.containsKey("province")){
province = addressJson.getString("province");
}
if(addressJson.containsKey("city")){
city = addressJson.getString("city");
}
}
}
if(StringUtils.isBlank(province) && StringUtils.isBlank(city)){
logger.error("【百度接口查询定位】------->查询结果为空。");
return null;
}
if(ipDto == null || ipDto.getId() == null){
BaiduIpLocationModel baiduip = new BaiduIpLocationModel();
baiduip.setIp(ip);
baiduip.setProvince(province);
baiduip.setCity(city);
baiduIpLocationDao.insert(baiduip);
}else{
if(!StringUtils.isBlank(province)){
ipDto.setProvince(province);
}
if(!StringUtils.isBlank(city)){
ipDto.setCity(city);
}
baiduIpLocationDao.updateById(ipDto);
}
adArea.setProvince(province);
adArea.setCity(city);
return adArea;
}
}
因为项目是多台部署的,所以就想着通过分布式锁来解决,可重入的分布式锁代码如下:
import java.util.HashMap;
import java.util.Map;
public class ThreadLocalUtil {
private static ThreadLocal<Map<String,Integer>> local = new ThreadLocal<Map<String,Integer>> ();
public static void set(String key,int value){
Map<String,Integer> map = local.get();
if (map == null ) {
map = new HashMap<String,Integer>();
local.set(map);
}
map.put(key, value);
}
public static Integer get(String key){
Map<String,Integer> map = local.get();
if (map == null ) {
map = new HashMap<String,Integer>();
local.set(map);
}
return map.get(key);
}
public static void remove(String key){
Map<String,Integer> map = local.get();
if (map == null ) {
map = new HashMap<String,Integer>();
local.set(map);
}
map.remove(key);
}
}
public class DistributedLock {
private static CustomCacheClient client = (CustomCacheClient) SpringContextsUtil.getBean("defaultCache",
CustomCacheClient.class);
public static void lock(String key, long expireTime) {
Integer integer = ThreadLocalUtil.get(key);
if (integer != null) {
ThreadLocalUtil.set(key, ++integer);
return;
}
CacheClient nativeCache = (CacheClient) client.getNativeCache();
while (true) {
if (nativeCache.add(key, 1, expireTime)) {
ThreadLocalUtil.set(key, 1);
break;
}
}
}
public static void unlock(String key) {
Integer integer = ThreadLocalUtil.get(key);
if (integer != null && integer > 1) {
ThreadLocalUtil.set(key, --integer);
} else {
ThreadLocalUtil.remove(key);
client.evict(key);
}
}
}
代码中增加分布式锁如下
@Service
@Transactional
public class AdBaiduLocationServiceImpl implements AdBaiduLocationService{
@Resource(name = "appConfig")
private AppConfig appConfig;
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource(name = "baiduIpLocationDao")
private BaiduIpLocationDao baiduIpLocationDao;
@Override
public AdAreaDto getLocatedAreaByIp(String ip) {
if(StringUtils.isBlank(ip)){
logger.info("【百度接口查询定位】------->缺少必要参数ip");
return null;
}
BaiduIpLocationModel ipDto = baiduIpLocationDao.findByIp(ip);
AdAreaDto adArea = new AdAreaDto();
if(ipDto != null && !StringUtils.isBlank(ipDto.getCity()) && !StringUtils.isBlank(ipDto.getProvince())){
adArea.setProvince(ipDto.getProvince());
adArea.setCity(ipDto.getCity());
return adArea;
}
DistributedLock.lock("LOCK" + ip, 10);
try {
ipDto = baiduIpLocationDao.findByIp(ip);
if (ipDto != null && !StringUtils.isBlank(ipDto.getCity()) && !StringUtils.isBlank(ipDto.getProvince())) {
adArea.setProvince(ipDto.getProvince());
adArea.setCity(ipDto.getCity());
return adArea;
}
String locationUrl = appConfig.get("baidu.map.location.ip.url");
String appKey = appConfig.get("baidu.map.location.secretKey");
if (StringUtils.isBlank(locationUrl) || StringUtils.isBlank(appKey)) {
logger.error("【百度接口查询定位】------->配置文件baidu.map.location.ip.url和baidu.map.location.secretKey参数缺失。");
return null;
}
Map<String, String> map = new HashMap<String, String>();
map.put("ak", appKey);
map.put("ip", ip);
String responseJson = HttpClientUtils.requestHttpPostMethod(locationUrl, map,
appConfig.get("advert.user.adInsideUrl"));
if (StringUtils.isBlank(responseJson)) {
throw new BusinessException(new ErrorCode("baidu location", "baidu location failed"));
}
JSONObject jsonObject = JSONObject.parseObject(responseJson);
String province = null;
String city = null;
if (jsonObject.containsKey("content")) {
JSONObject contentJson = jsonObject.getJSONObject("content");
if (contentJson.containsKey("address_detail")) {
JSONObject addressJson = contentJson.getJSONObject("address_detail");
if (addressJson.containsKey("province")) {
province = addressJson.getString("province");
}
if (addressJson.containsKey("city")) {
city = addressJson.getString("city");
}
}
}
if (StringUtils.isBlank(province) && StringUtils.isBlank(city)) {
logger.error("【百度接口查询定位】------->查询结果为空。");
return null;
}
if (ipDto == null || ipDto.getId() == null) {
BaiduIpLocationModel baiduip = new BaiduIpLocationModel();
baiduip.setIp(ip);
baiduip.setProvince(province);
baiduip.setCity(city);
baiduIpLocationDao.insert(baiduip);
} else {
if (!StringUtils.isBlank(province)) {
ipDto.setProvince(province);
}
if (!StringUtils.isBlank(city)) {
ipDto.setCity(city);
}
baiduIpLocationDao.updateById(ipDto);
}
adArea.setProvince(province);
adArea.setCity(city);
} finally {
DistributedLock.unlock("LOCK" + ip);
}
return adArea;
}
}
此代码感觉没什么问题,后面把IP字段也设置为唯一索引,上线后发现很小几率会报Duplicate entry '171.8.132.4' for key 'baidu_ip' 之类的错误,第一直觉是分布式锁没生效,然后测试发现是可以锁住的,那为什么还会有重复插入呢。后面发现是因为AdBaiduLocationServiceImpl 类加了Transactional开启了事务,导致一线程插入数据,执行完解锁,但未返回没有提交事务时,其他线程这时得到锁再查询数据库,此时还是查询不到的,进而导致插入的时候冲突而报错。此处完全不需要进行事务控制,所以删除事务就可以了。