记一次想利用分布式锁解决数据并发安全问题,但最后因为事务导致失败的问题

在生产上发现有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开启了事务,导致一线程插入数据,执行完解锁,但未返回没有提交事务时,其他线程这时得到锁再查询数据库,此时还是查询不到的,进而导致插入的时候冲突而报错。此处完全不需要进行事务控制,所以删除事务就可以了。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值