函数式编程小试-记一次代码重构

背景

有个需求是在设备上绑定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;
}

结语:
函数式编程还是能大大减少重复逻辑吧。

上一篇:构建高可用服务器之四 Keepalive冗余Nginx


下一篇:Java EE WEB工程师培训-JDBC+Servlet+JSP整合开发之17.Session