应用场景##
- 数据访问采用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
博客搬家:大坤的个人博客
欢迎评论哦~