自定义注解&Spring AOP实现为原程序加入Redis缓存支持(可重用)

应用场景##

  • 数据访问采用ORM方式(Hibernate) 直接访问数据库,在访问量小、并发性小、数据量小时,可正常访问,反之则服务响应能力低。
  • 福利彩蛋

目标&要解决的问题##

  • 自定义注解&Spring AOP为项目加入Redis缓存依赖提高应用程序的响应能力(可重用)

项目扩充承接于http://www.jianshu.com/p/25039d901ac2

难点##

设置缓存的失效策略,缓存数据的Struct选取,切面(Aspect)的编写

方法&扩充步骤##

1.扩充build.gradle 脚本文件

    //https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis 项目添加redis支持
    compile group: 'org.springframework.data', name: 'spring-data-redis', version: '1.4.1.RELEASE'
    // https://mvnrepository.com/artifact/redis.clients/jedis redis 基于java的Redis客户端调用实现
    compile group: 'redis.clients', name: 'jedis', version: '2.6.1'
    // https://mvnrepository.com/artifact/com.alibaba/fastjson
    // 采用阿里巴巴fastjson 进行对象&json字符串的序列化与反序列化
    compile group: 'com.alibaba', name: 'fastjson', version: '1.2.21'

2.扩充Spring 配置文件,添加Redis相关Java Bean 到Ioc容器中
为了符合开闭原则,重新创建Spring 配置文件 spring-redis.xml




    
        
       
        
    

    

    
        
    


3.自定义两个注解

  • RedisCahe: 标识缓存 注解
  • RedisEvit: 标识缓存清除 注解

代码如下:
RedisCahe.java

package com.fxmms.common.rediscache.redisannotation;

import java.lang.annotation.*;

/**
 * Created by mark on 16/11/29.
 * @usage  缓存注解类
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisCache {
    Class type();//被代理类的全类名,在之后会做为redis hash 的key
}

RedisEvit.java

package com.fxmms.common.rediscache.redisannotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by mark on 16/11/29.
 * @usage 清除过期缓存注解,放置于update delete insert 类型逻辑之上
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisEvict {
    Class type();
}

4.RedisCacheAspect.java 切面程序

package com.fxmms.common.rediscache.redisaspect;

import com.fxmms.common.rediscache.redisannotation.RedisCache;
import com.fxmms.common.rediscache.redisannotation.RedisEvict;
import com.fxmms.common.util.FastJsonUtil;
import com.fxmms.common.util.JsonUtil;
import org.apache.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.List;

/**
 * Created by mark on 16/11/29.
 */
@Aspect
@Component
@SuppressWarnings(value = {"rawtypes", "unchecked"})
public class RedisCacheAspect {

    private static final Logger logger = Logger.getLogger(RedisCacheAspect.class);
    /**
     * 分隔符 生成key 格式为 类全类名|方法名|参数所属类全类名
     **/
    private static final String DELIMITER = "|";
    /**
     * spring-redis.xml配置连接池、连接工厂、Redis模板
     **/
    @Autowired
    @Qualifier("redisTemplateForString")
    StringRedisTemplate srt;

    /**
     * Service层切点 使用到了我们定义的 RedisCache 作为切点表达式。
     * 而且我们可以看出此表达式基于 annotation。
     * 并且用于内建属性为查询的方法之上
     */
    @Pointcut("@annotation(com.fxmms.common.rediscache.redisannotation.RedisCache)")
    public void redisCacheAspect() {
    }

    /**
     * Service层切点 使用到了我们定义的 RedisEvict 作为切点表达式。
     * 而且我们可以看出此表达式是基于 annotation 的。
     * 并且用于内建属性为非查询的方法之上,用于更新表
     */
    @Pointcut("@annotation(com.fxmms.common.rediscache.redisannotation.RedisEvict)")
    public void redisCacheEvict() {
    }

    @Around("redisCacheAspect()")
    public Object cache(ProceedingJoinPoint joinPoint) {
        // 得到类名、方法名和参数
        String clazzName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();

        // 根据类名、方法名和参数生成Key
        logger.info("key参数: " + clazzName + "." + methodName);
        //System.out.println("key参数: " + clazzName + "." + methodName);
        String key = getKey(clazzName, methodName, args);
        if (logger.isInfoEnabled()) {
            logger.info("生成key: " + key);
        }

        // 得到被代理的方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

        // 得到被代理的方法上的注解
        Class modelType = method.getAnnotation(RedisCache.class).type();

        // 检查Redis中是否有缓存
        String value = (String) srt.opsForHash().get(modelType.getName(), key);

        // 得到被代理方法的返回值类型
        Class returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType();

        // result是方法的最终返回结果
        Object result = null;
        try {
            if (null == value) {
                if (logger.isInfoEnabled()) {
                    logger.info("缓存未命中");
                }

                // 调用数据库查询方法
                result = joinPoint.proceed(args);

                // 序列化查询结果
                String json = FastJsonUtil.toJsonString(result);
                //String json = GsonUtil.toJson(result);
                System.out.println("打印:"+json);

                // 序列化结果放入缓存
                srt.opsForHash().put(modelType.getName(), key, json);
            } else {

                // 缓存命中
                if (logger.isInfoEnabled()) {
                    logger.info("缓存命中, value = " + value);
                }

                result = value;
                // 反序列化 从缓存中拿到的json字符串
                result = FastJsonUtil.toObject(value, returnType);
                //result = GsonUtil.fromJson(value,returnType);
                System.out.println(result.toString());

                if (logger.isInfoEnabled()) {
                    logger.info("gson反序列化结果 = " + result);
                }
            }
        } catch (Throwable e) {
            logger.error("解析异常",e);
        }
        return result;
    }

    /**
     *      * 在方法调用前清除缓存,然后调用业务方法
     *      * @param joinPoint
     *      * @return
     *      * @throws Throwable
     *      
     */
    @Around("redisCacheEvict()")
    public Object evictCache(ProceedingJoinPoint joinPoint) throws Throwable {
        // 得到被代理的方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        // 得到被代理的方法上的注解
        Class modelType = method.getAnnotation(RedisEvict.class).type();
        if (logger.isInfoEnabled()) {
            logger.info("清空缓存 = " + modelType.getName());
        }
        // 清除对应缓存
        srt.delete(modelType.getName());
        return joinPoint.proceed(joinPoint.getArgs());
    }

    /**
     * @param json
     * @param clazz
     * @param modelType
     * @return 反序列化json字符串
     * Question 遇到问题,如何将复杂json字符串解析为复杂java object
     */
    private Object deserialize(String json, Class clazz, Class modelType) {
        // 序列化结果是List对象
        if (clazz.isAssignableFrom(List.class)) {
            return JsonUtil.jsonToList(json, modelType);
        }
        // 序列化结果是普通对象
        return JsonUtil.jsonToPojo(json, clazz);
    }

    private String serialize(Object result, Class clazz) {
        return JsonUtil.objectToJson(result);
    }

    /**
     *      * 根据类名、方法名和参数生成Key
     *      * @param clazzName
     *      * @param methodName
     *      * @param args
     *      * @return key格式:全类名|方法名|参数类型
     *      
     */
    private String getKey(String clazzName, String methodName, Object[] args) {
        StringBuilder key = new StringBuilder(clazzName);
        key.append(DELIMITER);
        key.append(methodName);
        key.append(DELIMITER);

        for (Object obj : args) {
            key.append(obj.getClass().getSimpleName());
            key.append(DELIMITER);
        }

        return key.toString();
    }
}

5.FastJsonUtil.java

 package com.fxmms.common.util;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.serializer.ValueFilter;

import java.util.List;

/**
 * Created by mark on 16/11/30.
 * 采用阿里巴巴fastjson 进行对象&json字符串的序列化与反序列化
 */
public class FastJsonUtil {
    /**
     * @param object
     * @return 将java对象转化为json字符串
     */
    public static String toJsonString(Object object) {
        return JSON.toJSONString(object,filter,SerializerFeature.DisableCircularReferenceDetect);
    }

    /**
     * 添加过滤器使数据库中字段为NULL的字段为""
     */
    private static ValueFilter filter = new ValueFilter() {
        @Override
        public Object process(Object obj, String s, Object v) {
            if (v == null)
                return "";
            return v;
        }
    };
    /**
     * @param json
     * @param cla
     * @param 
     * @return 将json字符串转化为java对象
     */
    public static  T toObject(String json, Class cla) {
        return JSON.parseObject(json, cla);
    }

    public static  List toList(String json, Class t) {
        return JSON.parseArray(json, t);
    }

}

6.业务逻辑层设置缓存即扩充service-applicationContext.xml加入切面支持





    

    
    
    
        
    
    
    
        
    
    
    
        
    
    
    

    
        
    


7.业务逻辑层应用缓存

package com.fxmms.www.service;

import com.fxmms.common.jniutil.GetDownloadIDUtil;
import com.fxmms.common.macutil.CountBetweenMacByMacStr;
import com.fxmms.common.poiutil.ReadExcelUtil;
import com.fxmms.common.rediscache.redisannotation.RedisCache;
import com.fxmms.common.rediscache.redisannotation.RedisEvict;
import com.fxmms.common.ro.ControllerResult;
import com.fxmms.common.ro.DtoResultWithPageInfo;
import com.fxmms.www.dao.AdminDao;
import com.fxmms.www.dao.MacDao;
import com.fxmms.www.dao.TaskDao;
import com.fxmms.www.domain.Admin;
import com.fxmms.www.domain.Mac;
import com.fxmms.www.domain.Task;
import com.fxmms.www.dto.MacDto;
import com.fxmms.www.qo.MacQo;
import com.fxmms.www.thunderinterfaceutil.VisitThunderInterface;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Created by mark on 16/11/7.
 *
 * @usage Mac地址操作业务逻辑层
 */
@Service
public class MacService {
    @Autowired
    MacDao macDao;
    @Autowired
    AdminDao adminDao;
    @Autowired
    TaskDao taskDao;

    /**
     * @param macStr
     * @param username
     * @return mac
     * @usage 判断数据库中是否已经存储过对应的mac
     * 防止数据库中存储多个同样的mac地址
     */
    @Transactional
    @RedisEvict(type=Mac.class)
    public Mac doJudgementBySingleMacStr(String macStr, String username) {
        Mac mac = macDao.getByUniqueKey("macAddr", macStr);
        if (mac == null) {
            //1.单个mac地址转化为downloadId
            String downLoadId = GetDownloadIDUtil.getDownLoadId(macStr);
            Task task = new Task();//单个mac所属task's id
            task.setDate(new Date());
            task.setFlag(0);//录入未成功
            taskDao.save(task);
            Admin admin = adminDao.getByUniqueKey("userName", username);
            mac = new Mac();
            mac.setDownLoadId(downLoadId);
            mac.setAdmin(admin);
            mac.setMacAddr(macStr);
            mac.setDate(new Date());
            //设置mac状态为init状态
            mac.setStatus(0);
            mac.setTask(task);
            macDao.save(mac);
        }
        return mac;
    }

    /**
     * @param macStrList
     * @param username
     * @usage 判断数据库中是否已经存储过对应的mac
     * 防止数据库中存储多个同样的mac地址
     */
    @Transactional
    @RedisEvict(type=Mac.class)
    public void doJudgementBySeriseMacStr(List macStrList, String username) {
        Task task = new Task();//单个mac所属task's id
        task.setDate(new Date());
        task.setFlag(0);//初始化task 状态为录入未成功
        for (String macStr : macStrList) {
            Mac mac = macDao.getByUniqueKey("macAddr", macStr);
            if (mac == null) {
                //1.单个mac地址转化为downloadId
                String downLoadId = GetDownloadIDUtil.getDownLoadId(macStr);
                taskDao.save(task);
                Admin admin = adminDao.getByUniqueKey("userName", username);
                mac = new Mac();
                mac.setDownLoadId(downLoadId);
                mac.setAdmin(admin);
                mac.setMacAddr(macStr);
                mac.setDate(new Date());
                //设置mac状态为init状态
                mac.setStatus(0);
                mac.setTask(task);
                macDao.save(mac);
            }
        }
    }

    /**
     * @param macStr
     * @param username
     * @return 1.单个mac地址转化为downloadId, 并调用迅雷方接口
     * 2.调用接口之前先将地址存储为数据库中一条记录,状态置为0-初始化状态
     * 3.调用完接口根据返回状态,将返回状态为success的数据置为1-正在录入
     */
    @Transactional
    @RedisEvict(type=Mac.class)
    public ControllerResult addSingleMac(String macStr, String username) {
        if (macStr == null || ("".equals(macStr))) {
            return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,MAC地址不能为空");
        }
        if (!CountBetweenMacByMacStr.matchMacAddrByregex(macStr)) {
            return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,MAC地址格式不正确");
        }
        List macStrList = new ArrayList<>();
        macStrList.add(macStr);
        Mac mac = doJudgementBySingleMacStr(macStr, username);
        //调用迅雷录入接口。
        if (VisitThunderInterface.addDownLoadId(macStrList)) {
            Admin admin = adminDao.getByUniqueKey("userName", username);
            if (mac.getStatus() != 2) {
                mac.setStatus(1);
                mac.setDate(new Date());
                mac.setAdmin(admin);
                macDao.update(mac);
            }
            return ControllerResult.valueOf(ControllerResult.SUCCESS, "迅雷录入接口请求成功", mac);
        } else {
            Admin admin = adminDao.getByUniqueKey("userName", username);
            if (mac.getStatus() != 2) {
                mac.setStatus(3);
                mac.setDate(new Date());
                mac.setAdmin(admin);
                macDao.update(mac);
                return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,请求迅雷录入接口失败!重新录入");
            }
            return ControllerResult.valueOf(ControllerResult.ERROR, "此条mac地址已经录入成功");
        }
    }

    /**
     * @param startMacStr
     * @param endMacStr
     * @param username
     * @return
     * @usage 批量区间录入业务逻辑方法
     */
    @Transactional
    @RedisEvict(type=Mac.class)
    public ControllerResult addSeriseMac(String startMacStr, String endMacStr, String username) {
        if (startMacStr == null || ("".equals(startMacStr)) || endMacStr == null || ("".equals(endMacStr))) {
            return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,MAC地址不能为空");
        }
        if (!CountBetweenMacByMacStr.matchMacAddrByregex(startMacStr) || !CountBetweenMacByMacStr.matchMacAddrByregex(endMacStr)) {
            return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,MAC地址格式不正确");
        }
        List macStrList = CountBetweenMacByMacStr.countBetweenMacByMacStr(startMacStr, endMacStr);
        if (macStrList.size() > 1000) {
            return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,MAC区间太长,请拆分后录入。重新录入");
        }
        doJudgementBySeriseMacStr(macStrList, username);
        if (VisitThunderInterface.addDownLoadId(macStrList)) {
            for (String macStr : macStrList) {
                Mac mac = macDao.getByUniqueKey("macAddr", macStr);
                Admin admin = adminDao.getByUniqueKey("userName", username);
                if (mac.getStatus() != 2) {
                    mac.setStatus(1);
                    mac.setDate(new Date());
                    mac.setAdmin(admin);
                    macDao.update(mac);
                }
            }
            return ControllerResult.valueOf(ControllerResult.SUCCESS, "录入成功");
        } else {
            for (String macStr : macStrList) {
                Mac mac = macDao.getByUniqueKey("macAddr", macStr);
                Admin admin = adminDao.getByUniqueKey("userName", username);
                if (mac.getStatus() != 2) {
                    mac.setStatus(3);
                    mac.setDate(new Date());
                    mac.setAdmin(admin);
                    macDao.update(mac);
                }
            }
            return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,请求迅雷录入接口失败!重新录入");
        }
    }

    /**
     * @param macQo
     * @return
     * @usage 获取所有的mac录入状态数据业务逻辑方法
     */
    @RedisCache(type=Mac.class)
    @Transactional
    public ControllerResult getAllMacStatus(MacQo macQo) {
        DtoResultWithPageInfo info = macDao.queryPageListByCriteriaWithQo(macQo, MacDto.class);
        return ControllerResult.valueOf(ControllerResult.SUCCESS, "获取mac录入状态成功", info);
    }

    /**
     * @param serverFile
     * @param username
     * @return
     * @usage 非连续mac地址录入逻辑方法
     */
    @Transactional
    @RedisEvict(type=Mac.class)
    public ControllerResult addNoOrderMac(File serverFile, String username) {
        ReadExcelUtil readExcelUtil = new ReadExcelUtil();
        try {
            List macStrList = readExcelUtil.readUploadMacFile(serverFile);
            if (macStrList.size() == 0 || macStrList == null) {
                return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,文件中MAC数据不能为空");
            }
            if (macStrList.size() > 1000) {
                return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,文件中数据超过1000条,请进行拆分后上传!");
            }
            for (String inFilemacStr : macStrList) {
                if (!CountBetweenMacByMacStr.matchMacAddrByregex(inFilemacStr)) {
                    return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,文件中有不合法的MAC地址");
                }
            }
            doJudgementBySeriseMacStr(macStrList, username);
            if (VisitThunderInterface.addDownLoadId(macStrList)) {
                for (String macStr : macStrList) {
                    Mac mac = macDao.getByUniqueKey("macAddr", macStr);
                    Admin admin = adminDao.getByUniqueKey("userName", username);
                    if (mac.getStatus() != 2) {
                        mac.setStatus(1);
                        mac.setDate(new Date());
                        mac.setAdmin(admin);
                        macDao.update(mac);
                    }
                }
                return ControllerResult.valueOf(ControllerResult.SUCCESS, "请求迅雷录入接口成功");
            } else {
                for (String macStr : macStrList) {
                    Mac mac = macDao.getByUniqueKey("macAddr", macStr);
                    Admin admin = adminDao.getByUniqueKey("userName", username);
                    if (mac.getStatus() != 2) {
                        mac.setStatus(3);
                        mac.setAdmin(admin);
                        mac.setDate(new Date());
                        macDao.update(mac);
                    }
                }
                return ControllerResult.valueOf(ControllerResult.ERROR, "对不起,请求迅雷录入接口失败!重新录入");
            }
        } catch (Exception e) {
            return ControllerResult.valueOf(ControllerResult.ERROR, "文件上传失败");
        }

    }
}

注意:

  • 上述程序中为非查询方法上加上了 @RedisEvict注解,表示删除旧的缓存。
  • 上述程序中为查询方法上加上了 @RedisCache注解,表示为查询业务逻辑应用缓存,应用逻辑为:项目中缓存数据的Struct为Hash,每张表对应的实体类使用一个名为Key的Hash结构来存储数据,当访问的key 存在时,直接从缓存中取出数据,不存在时第一步先从数据库中查询数据,再生成key,并生成对应的filed与value。

程序运行结果:

2016-12-03 20:16:05,212 [INFO]-[com.fxmms.common.rediscache.redisaspect.RedisCacheAspect.cache(RedisCacheAspect.java:67)] key参数: com.fxmms.www.service.MacService.getAllMacStatus
2016-12-03 20:16:05,219 [INFO]-[com.fxmms.common.rediscache.redisaspect.RedisCacheAspect.cache(RedisCacheAspect.java:71)] 生成key: com.fxmms.www.service.MacService|getAllMacStatus|MacQo|
2016-12-03 20:16:05,357 [INFO]-[com.fxmms.common.rediscache.redisaspect.RedisCacheAspect.cache(RedisCacheAspect.java:108)] 缓存命中, value = {"msg":"获取mac录入状态成功","result":"success","rows":{"emptyResult":false,"pageInfo":{"firstPage":true,"firstResultNum":0,"lastPage":false,"lastResultNum":10,"pageNo":1,"pageSize":10,"totalPage":49,"totalQuantity":488},"results":[{"admin":{"enable":1,"id":1,"isDelete":0,"password":"11","role":"admin","userName":"ls"},"date":1479913221000,"dateStr":"2016-11-23 23:00:21","deviceId":"730CBAEA-6954-000A-2D77-BAF544E6F192","downLoadId":"11123E566745FB30FE5C9AC094A1BAA0","id":488,"macAddr":"11:12:3e:56:67:45","status":2,"statusStr":"录入成功","task":{"date":1479913220000,"flag":1,"id":29}},{"admin":{"enable":1,"id":1,"isDelete":0,"password":"11","role":"admin","userName":"ls"},"date":1479448899000,"dateStr":"2016-11-18 14:01:39","deviceId":"","downLoadId":"34BDF9C0B2C1EC6B5CA3B81DCB05241D","id":487,"macAddr":"34:BD:F9:C0:B2:c1","status":3,"statusStr":"录入失败","task":{"date":1479448898000,"flag":0,"id":28}},{"admin":{"enable":1,"id":1,"isDelete":0,"password":"11","role":"admin","userName":"ls"},"date":1479448476000,"dateStr":"2016-11-18 13:54:36","deviceId":"","downLoadId":"11123E586745088C6CAF8E6C2EBDB7A5","id":486,"macAddr":"11:12:3e:58:67:45","status":3,"statusStr":"录入失败","task":{"date":1479448476000,"flag":0,"id":27}},{"admin":{"enable":1,"id":1,"isDelete":0,"password":"11","role":"admin","userName":"ls"},"date":1479447598000,"dateStr":"2016-11-18 13:39:58","deviceId":"","downLoadId":"34BDFAC0B2F01A731572C0BCEC4D26F0","id":485,"macAddr":"34:BD:FA:C0:B2:F0","status":3,"statusStr":"录入失败","task":{"date":1479447598000,"flag":0,"id":26}},{"admin":{"enable":1,"id":1,"isDelete":0,"password":"11","role":"admin","userName":"ls"},"date":1479447575000,"dateStr":"2016-11-18 13:39:35","deviceId":"","downLoadId":"3EBDF9C0B2F02D7F2A6CAC4F2B5121E8","id":484,"macAddr":"3e:BD:F9:C0:B2:F0","status":3,"statusStr":"录入失败","task":{"date":1479447575000,"flag":0,"id":25}},{"admin":{"enable":1,"id":1,"isDelete":0,"password":"11","role":"admin","userName":"ls"},"date":1479446783000,"dateStr":"2016-11-18 13:26:23","deviceId":"","downLoadId":"11128E566749F8776504253D15D8B001","id":483,"macAddr":"11:12:8e:56:67:49","status":3,"statusStr":"录入失败","task":{"date":1479446783000,"flag":0,"id":24}},{"admin":{"enable":1,"id":1,"isDelete":0,"password":"11","role":"admin","userName":"ls"},"date":1479446754000,"dateStr":"2016-11-18 13:25:54","deviceId":"","downLoadId":"11128E566745B130B2E6C6AA8E52EB4A","id":482,"macAddr":"11:12:8e:56:67:45","status":3,"statusStr":"录入失败","task":{"date":1479446753000,"flag":0,"id":23}},{"admin":{"enable":1,"id":1,"isDelete":0,"password":"11","role":"admin","userName":"ls"},"date":1479446736000,"dateStr":"2016-11-18 13:25:36","deviceId":"","downLoadId":"341DF9C0B2F11E391DDA8EDAB78B4162","id":481,"macAddr":"34:1D:F9:C0:B2:F1","status":3,"statusStr":"录入失败","task":{"date":1479446736000,"flag":0,"id":22}},{"admin":{"enable":1,"id":1,"isDelete":0,"password":"11","role":"admin","userName":"ls"},"date":1479437904000,"dateStr":"2016-11-18 10:58:24","deviceId":"","downLoadId":"11446633889613659EE26ABE4FBE28CD","id":480,"macAddr":"11:44:66:33:88:96","status":3,"statusStr":"录入失败","task":{"date":1479437904000,"flag":0,"id":21}},{"admin":{"enable":1,"id":1,"isDelete":0,"password":"11","role":"admin","userName":"ls"},"date":1479437899000,"dateStr":"2016-11-18 10:58:19","deviceId":"","downLoadId":"1144663388947CCC987231F802C72F83","id":479,"macAddr":"11:44:66:33:88:94","status":3,"statusStr":"录入失败","task":{"date":1479437899000,"flag":0,"id":20}}]},"total":0}

完。

福利彩蛋

职位:腾讯OMG 广告后台高级开发工程师;
Base:深圳;
场景:海量数据,To B,To C,场景极具挑战性。
基础要求:
熟悉常用数据结构与算法;
熟悉常用网络协议,熟悉网络编程;
熟悉操作系统,有线上排查问题经验;
熟悉MySQL,oracle;
熟悉JAVA,GoLang,c++其中一种语言均可;
可内推,欢迎各位优秀开发道友私信[微笑]
期待关注我的开发小哥哥,小姐姐们私信我,机会很好,平台对标抖音,广告生态平台,类似Facebook 广告平台,希望你们用简历砸我~
联系方式 微信 13609184526

博客搬家:大坤的个人博客
欢迎评论哦~

上一篇:一分钟“零代码”生成API,DataWorks数据服务快速上手指南


下一篇:c#线程--生产者和消费者