背景
有个需求是在设备上绑定service和解除对service的绑定。
设计:
数据库设计是一张设备表,表上用一个列unbind_service来代表解除绑定的services。
那么我最开始写了如下代码:
private boolean bindService(String appkey, String deviceId, String service) {
DeviceDO deviceDO = new DeviceDO();
deviceDO.setAppkey(appkey);
deviceDO.setDeviceId(deviceId);
DeviceDO dbDeviceDO = this.findDevice(appkey, deviceId);
if(dbDeviceDO==null){
return this.insertDevice(deviceDO);
}else {
Set<String> services = unbindServiceToSet(dbDeviceDO.getUnbindService());
services.remove(service);
String toSaveService = mapUnbindService(services);
dbDeviceDO.setUnbindService(toSaveService);
int count = sqlExecutor.update(dbDeviceDO, Lists.newArrayList(ColumnConstants.GMT_CREATE),
SqlBeanUtil.getStatementPartList(
deviceDO, ColumnConstants.APPKEY, ColumnConstants.DEVICE_ID));
return count>=1 ? true: false;
}
}
public boolean unbindService(String appkey, String deviceId, String service){
DeviceDO deviceDO = new DeviceDO();
deviceDO.setAppkey(appkey);
deviceDO.setDeviceId(deviceId);
DeviceDO dbDeviceDO = this.findDevice(appkey, deviceId);
if(dbDeviceDO==null){
return this.insertDevice(deviceDO);
}else {
Set<String> services = unbindServiceToSet(dbDeviceDO.getUnbindService());
services.add(service);
String toSaveService = mapUnbindService(services);
dbDeviceDO.setUnbindService(toSaveService);
int count = sqlExecutor.update(dbDeviceDO, Lists.newArrayList(ColumnConstants.GMT_CREATE),
SqlBeanUtil.getStatementPartList(
deviceDO, ColumnConstants.APPKEY, ColumnConstants.DEVICE_ID));
return count>=1 ? true: false;
}
}
写完之后一看,发现unbindService和bindService方法都是先检查数据库中是否存在对应的设备,没有的话,就插入一条新记录。已经存在对应设备了,就更新unbindService,再update 设备。
所以可以合并一下,新的代码如下:
private boolean bindService(String appkey, String deviceId, String service) {
return bindOrUnbindService(appkey, deviceId, service, false);
}
private boolean unbindService(String appkey, String deviceId, String service) {
return bindOrUnbindService(appkey, deviceId, service, true);
}
private boolean bindOrUnbindService(String appkey, String deviceId, String service, boolean isUnbind) {
DeviceDO deviceDO = new DeviceDO();
deviceDO.setAppkey(appkey);
deviceDO.setDeviceId(deviceId);
DeviceDO dbDeviceDO = deviceSDK.findDevice(appkey, deviceId);
if (dbDeviceDO == null) {
if(isUnbind){
deviceDO.setUnbindService(service);
}
return deviceSDK.insertDevice(deviceDO);
} else {
Set<String> services = unbindService(dbDeviceDO.getUnbindService());
if (isUnbind) {
services.add(service);
} else {
//it's bindding
services.remove(service);
}
String toSaveService = mapUnbindService(services);
dbDeviceDO.setUnbindService(toSaveService);
int count = sqlExecutor.update(dbDeviceDO, Lists.newArrayList(ColumnConstants.GMT_CREATE),
SqlBeanUtil.getStatementPartList(
deviceDO, ColumnConstants.APPKEY, ColumnConstants.DEVICE_ID));
return count >= 1 ? true : false;
}
}
好了,再看看bindOrUnbindService方法,多线程并发情况下,很有可能产生问题。那么最好给它加上事务或者全局锁。写上事务的话,sqlExecutor写起来会比较麻烦。所以我偷懒点,选择使用全局锁,使用redis做全局锁,redis 客户端 RedissonClient就有对锁的支持。于是上述代码又被修改成:
public boolean bindService(String appkey, String deviceId, String service){
boolean success = false;
if(redisClient!=null){
String lockName = "bind_service_"+appkey+deviceId;
RLock lock = redisClient.getLock(lockName);
if(lock!=null){
try {
if(lock.tryLock(1000, TimeUnit.MILLISECONDS)){
success = bindServiceInternal(appkey, deviceId, service);
String key = getUnbindServiceCacheKey(appkey, deviceId);
redisClient.getBucket(key).expire(0, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException e) {
DeviceSDKLogger.ERROR_LOGGER.error("bindService lock failed", e);
}finally {
lock.unlock();
}
}
}else {
success = bindServiceInternal(appkey, deviceId, service);
}
return success;
}
public boolean unbindService(String appkey, String deviceId, String service){
boolean success = false;
if(redisClient!=null){
String lockName = "bind_service_"+appkey+deviceId;
RLock lock = redisClient.getLock(lockName);
if(lock!=null){
try {
if(lock.tryLock(1000, TimeUnit.MILLISECONDS)){
success = unbindServiceInternal(appkey, deviceId, service);
String key = getUnbindServiceCacheKey(appkey, deviceId);
redisClient.getBucket(key).expire(0, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException e) {
DeviceSDKLogger.ERROR_LOGGER.error("unbindService lock failed", e);
}finally {
lock.unlock();
}
}
}else {
success = unbindServiceInternal(appkey, deviceId, service);
}
return success;
}
上述代码看上去95%是重复的,而且直接依赖了redis。重复代码java8之前就没辙了,在java8之后,我们可以使用函数式编程。在这里,我先把锁的逻辑抽象出来。
/**
* @author jingjing.zhijj
* @create 2018/6/20 下午4:00
*/
public interface LockSupport {
/**
* 执行一个函数,LockSupport实现保证全局排他
* @param lockKey 全局锁的key
* @param function 需要执行的函数
* @param <R> 执行函数的返回类型
* @return 待执行函数的执行结果
*/
<R> R execFunction(String lockKey, FunctionNeedLock<R> function);
}
使用一个新的接口有好处,以后可以换锁的实现。
虽然java8在java.util.function默认提供了几十接口,但是没有我需要的,所以我重新定义一下。
/**
* @author jingjing.zhijj
* @create 2018/6/20 下午3:46
*/
public interface FunctionNeedLock<R> {
/**
* 一个无参的Function
* @return
*/
R apply();
}
这个接口中其实名字是否是apply无所谓,只要方法的入参和返回值类型一致就可以。
可能有人会有疑问,bindService方法有3个参数,为何FuncitonNeedLock接口没有参数?哦,这个可以使用闭包解决。新的代码如下:
@Override
public boolean bindService(String appkey, String deviceId, String service) {
String lockName = "bind_service_" + appkey + deviceId;
return lockSupport.execFunction(lockName, () -> {
return bindServiceInternal(appkey, deviceId, service);
}
);
}
@Override
public boolean unbindService(String appkey, String deviceId, String service) {
String lockName = "bind_service_" + appkey + deviceId;
return lockSupport.execFunction(lockName, () -> {
return unbindServiceInternal(appkey, deviceId, service);
}
);
}
锁的实现代码如下:
/**
* @author jingjing.zhijj
* @create 2018/6/20 下午4:00
*/
public class RedisLockSupport implements LockSupport {
private RedissonClient redissonClient;
@Override
public <R> R execFunction(String lockKey, FunctionNeedLock<R> function) {
R result = null;
if (redissonClient != null) {
RLock lock = redissonClient.getLock(lockKey);
if (lock != null) {
try {
if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
result = function.apply();
}
} catch (InterruptedException e) {
DeviceSDKLogger.ERROR_LOGGER.error("failed execFunction", e);
} finally {
lock.unlock();
}
}
} else {
result = function.apply();
}
return result;
}
public void setRedissonClient(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
}
再运行一次单元测试,发现跑不过了,
@Test
public void testBindServiceImpl(){
String appkey = "myappkey";
String deviceId = "xxxxxxxxxxxxxxx";
boolean success = bindService.bindService(appkey, deviceId, "abc");
Set<String> unbindServiceSet = bindService.getUnbindService(appkey, deviceId);
Assert.assertTrue(!unbindServiceSet.contains("abc"));
success = bindService.unbindService(appkey, deviceId, "abc");
unbindServiceSet = bindService.getUnbindService(appkey, deviceId);
Assert.assertTrue(unbindServiceSet.contains("abc"));
}
原来是没有处理缓存失效。。以下代码把失效逻辑补回来。
@Override
public boolean bindService(String appkey, String deviceId, String service) {
String lockName = "bind_service_" + appkey + deviceId;
boolean success = lockSupport.execFunction(lockName, () -> {
return bindServiceInternal(appkey, deviceId, service);
}
);
if(success){
cacheProvider.remove(getUnbindServiceCacheKey(appkey, deviceId));
}
return success;
}
@Override
public boolean unbindService(String appkey, String deviceId, String service) {
String lockName = "bind_service_" + appkey + deviceId;
boolean success = lockSupport.execFunction(lockName, () -> {
return unbindServiceInternal(appkey, deviceId, service);
}
);
if(success){
cacheProvider.remove(getUnbindServiceCacheKey(appkey, deviceId));
}
return success;
}
结语:
函数式编程还是能大大减少重复逻辑吧。