【Redis使用系列】使用Redis做防止重复提交
2019-05-17 00:01:14来源:博客园 阅读 ()
前言
在平时的开发中我们都需要处理重复提交的问题,避免业务出错或者产生脏数据,虽然可以通过前端控制但这并不是可以完全避免,最好的方式还是前后端均进行控制,这样的话就可以更有效,尽可能全面的去减少错误的发生。
一、比如我们注册的时候需要发送验证码
如果用户频繁点击或者恶意攻击的话就会造成不断的请求对服务器产生很大的压力,为了避免这种情况我们需要做处理,传统的模式中是在数据库中记录手机号、验证码已经发送时间,再次请求的时候呢去数据库查询是否有该手机号记录,并校验是否超过间隔时间,如果超过则重新发送并更新时间,否组不予发送,这样有一个缺点就是如果同时又很多人在做相同的业务同时查询就会对数据库造成很大的压力。
根据此种情况我们可以使用Redis incrde 原子性递增,来解决这种高并发的秒杀或者分布式序列号生成等场景。鉴于本场景我们只用他来做计数实现间隔时间内只接收一次请求。
实现逻辑:在发送短信之后使用Redis的incr设置一个递增的KEY(根据自己的需要设定但是要保证每一个人的唯一),来判断该KEY的数值,如果等于1说明这是第一次请求,发送短信记录日志,并设置有效期,如果不等于的话说明是间隔时间内多次请求,就提示请求频繁,稍后重试。
1 String redisKey = "SMS_SEND_" + smsPhone; 2 long count = redisTemplate.opsForValue().increment(redisKey, 1); 3 if (count == 1) { 4 //设置有效期一分钟 5 redisTemplate.expire(redisKey, 60, TimeUnit.SECONDS); 6 } 7 if (count > 1) { 8 resultMap.put("retCode", "-1"); 9 resultMap.put("retMsg", "每分钟只能发送一次短信"); 10 outPrintJson(resultMap); 11 return; 12 } 13 /** 发送短信 */ 14 ...... 15 /** 记录发送日志 */ 16 ......
二、上述方式可以解决特定的问题,当需要处理的情况多的话我们可以考虑使用切面来解决
1 package com.slp.annotation; 2 3 public class RedisLockBean { 4 private String key; 5 private int timeInSecond; 6 private String codeName; 7 private String msgName; 8 private String code; 9 private String msg; 10 private boolean isAtController; 11 private boolean isAtService; 12 private boolean isAtParameter; 13 private String returnType; 14 public String getKey() { 15 return key; 16 } 17 public void setKey(String key) { 18 this.key = key; 19 } 20 public int getTimeInSecond() { 21 return timeInSecond; 22 } 23 public void setTimeInSecond(int timeInSecond) { 24 this.timeInSecond = timeInSecond; 25 } 26 public String getCodeName() { 27 return codeName; 28 } 29 public void setCodeName(String codeName) { 30 this.codeName = codeName; 31 } 32 public String getMsgName() { 33 return msgName; 34 } 35 public void setMsgName(String msgName) { 36 this.msgName = msgName; 37 } 38 public String getCode() { 39 return code; 40 } 41 public void setCode(String code) { 42 this.code = code; 43 } 44 public String getMsg() { 45 return msg; 46 } 47 public void setMsg(String msg) { 48 this.msg = msg; 49 } 50 51 52 public boolean isAtController() { 53 return isAtController; 54 } 55 public void setAtController(boolean isAtController) { 56 this.isAtController = isAtController; 57 } 58 public boolean isAtService() { 59 return isAtService; 60 } 61 public void setAtService(boolean isAtService) { 62 this.isAtService = isAtService; 63 } 64 public boolean isAtParameter() { 65 return isAtParameter; 66 } 67 public void setAtParameter(boolean isAtParameter) { 68 this.isAtParameter = isAtParameter; 69 } 70 public String getReturnType() { 71 return returnType; 72 } 73 public void setReturnType(String returnType) { 74 this.returnType = returnType; 75 } 76 @Override 77 public String toString() { 78 return "RedisLockBean [key=" + key + ", timeInSecond=" + timeInSecond 79 + ", codeName=" + codeName + ", msgName=" + msgName + ", code=" 80 + code + ", msg=" + msg + ", isAtController=" + isAtController 81 + ", isAtService=" + isAtService + ", isAtParameter=" 82 + isAtParameter + ", returnType=" + returnType + "]"; 83 } 84 }
1 package com.slp.annotation; 2 3 import java.lang.annotation.ElementType; 4 import java.lang.annotation.Retention; 5 import java.lang.annotation.RetentionPolicy; 6 import java.lang.annotation.Target; 7 /** 8 * 解决的问题:<br> 9 * 1.数据库加锁性能较差<br> 10 * 2.数据库加锁,若相应线程异常,所无法释放<br> 11 * 注意事项:<br> 12 * 方法的返回值对象必须包含错误码,错误信息属性及其的get方法 13 * 14 */ 15 @Target({ElementType.PARAMETER,ElementType.METHOD}) 16 @Retention(RetentionPolicy.RUNTIME) 17 public @interface RedisLock { 18 /** 19 * 若加注解的入参时基本数据类型(int,long)或String时,fieldName无效<br> 20 * 若注解的参数是自定义对象时,请注意一下几点:<br> 21 * 1.确保定义有相应属性public修饰的get方法<br> 22 * 2.get方法的返回参数是基本的数据类型或String<br> 23 * 3.get方法的返回值不为空<br> 24 * 否则,加锁失败. 25 * @return 26 */ 27 String[] fieldName() default {}; 28 /** 29 * 锁的有效时间,单位为秒,默认值为1 30 * @return 31 */ 32 int timeInSecond() default 1; 33 /** 34 * 加锁,锁已被其它请求获取时,直接返回重复提交,codeName指定返回对象的返回码对应的属性,默认值'code' 35 * @return 36 */ 37 String codeName() default "code"; 38 /** 39 * 加锁,锁已被其它请求获取时,直接返回重复提交,msgName指定返回对象的返回信息对应的属性,默认值'msg' 40 * @return 41 */ 42 String msgName() default "msg"; 43 /** 44 * 加锁,锁已被其它请求获取时,直接返回重复提交,code指定返回对象的返回码对应的值,默认值'09' 45 * @return 46 */ 47 String code() default "09"; 48 /** 49 * 加锁,锁已被其它请求获取时,直接返回重复提交,msg指定返回对象的返回码对应的值,默认值'重复提交' 50 * @return 51 */ 52 String msg() default "重复提交"; 53 /** 54 * 注解作用与方法时,指定参数在参数列表中的索引 55 */ 56 int paramIndex() default 0; 57 }
package com.slp.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.alibaba.fastjson.JSONObject; import com.cul.culsite.common.RedisKeyConstants; import com.cul.culsite.service.RedisService; import com.cul.culsite.util.DateUtil; import com.cul.culsite.util.OxmHelper; @Component public class RedisLockAspect { private final static Logger logger = LoggerFactory.getLogger(RedisLockAspect.class); protected static final String XML_TYPE = "xml"; protected static final String JSON_TYPE = "json"; protected static final String ILLEGAL_TYPE = "illegal type"; @Autowired private RedisService redisService; public Object redisLockParse(ProceedingJoinPoint p) throws Throwable{ Signature signature = p.getSignature(); boolean isRepetition = false; RedisLockBean redisLockBean = null; String value = System.nanoTime()+""; if(signature instanceof MethodSignature){ //获得接口中定义的方法的Method,但注解时加载实现类中方法的参数上 MethodSignature methodSignature = (MethodSignature)signature; Method serviceMethod = methodSignature.getMethod(); try { Method serviceImpMethod = p.getTarget().getClass().getMethod(serviceMethod.getName(), serviceMethod.getParameterTypes()); //获取key值 redisLockBean = getRedisLockKey(p.getTarget(),serviceImpMethod,p.getArgs()); //成功获取key值,在redis中加锁 if(redisLockBean!=null){ logger.info("redis lock value is :{}",value); boolean isPutSuccess =redisService.setIfAbsent(redisLockBean.getKey(), value, redisLockBean.getTimeInSecond()); //加锁失败,直接返回 if(!isPutSuccess){ logger.info("get redis lock fail for {}",redisLockBean.getKey()); if(redisLockBean.isAtParameter()||redisLockBean.isAtService()){ Class<?> returnType = serviceImpMethod.getReturnType(); //加锁方法有返回值 if(!returnType.getName().equals(java.lang.Void.class.getName())){ //实例化返回值对象 try { Object result = returnType.newInstance(); //设置返回码 returnType.getMethod(getSetMethodNameByFieldName(redisLockBean.getCodeName()), java.lang.String.class).invoke(result, redisLockBean.getCode()); //设置返回信息 returnType.getMethod(getSetMethodNameByFieldName(redisLockBean.getMsgName()), java.lang.String.class).invoke(result, redisLockBean.getMsg()); return result; } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }else{ throw new RuntimeException("@RedisLock作用的方法没有返回参数"); } }else if(redisLockBean.isAtController()){ Map<String,String> result = new HashMap<String,String>(); result.put(redisLockBean.getCodeName(), redisLockBean.getCode()); result.put(redisLockBean.getMsgName(), redisLockBean.getMsg()); return response(redisLockBean.getReturnType()==null?"json":redisLockBean.getReturnType(), result); } }else{ logger.info("get redis lock success for {}",redisLockBean.getKey()); isRepetition = true; } } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } } Object result = null; try { result = p.proceed(); } catch (Throwable e) { throw e; }finally{ if(redisLockBean!=null){ if(isRepetition&&value.equals(redisService.get(redisLockBean.getKey()))){ logger.info("lock has released :{}",redisLockBean.getKey()); redisService.delete(redisLockBean.getKey()); } } } return result; } private RedisLockBean getRedisLockKey(Object target,Method method,Object... object){ if(target == null){ throw new RuntimeException("get redis lock key error,target is null"); } if(method==null){ throw new RuntimeException("get redis lock key error,method is null"); } List<String> fieldValueList = new ArrayList<String>(); RedisLockBean redisLockBean = new RedisLockBean(); RedisLock redisLock = null; //类上有@Controller说明@RedisLock是放在请求方法上,使用HttpServletRequest获取请求参数 if(method.isAnnotationPresent(RedisLock.class)&&target.getClass().isAnnotationPresent(Controller.class)){ //controller层方法时对外开放的接口 if(method.isAnnotationPresent(RequestMapping.class)){ redisLock = method.getAnnotation(RedisLock.class); //获取方法中的HttpServletRequest类型的参数 HttpServletRequest request = null; for(Object para:object){ if(para instanceof HttpServletRequest){ request = (HttpServletRequest)para; break; } } if(request==null){ throw new RuntimeException("@RedisLock作用于controller层方法时,方法需要包含HttpServletRequest类型的参数"); } //未定义加锁参数时,默认使用mac String[] paraName = redisLock.fieldName(); if(paraName==null||paraName.length==0){ paraName=new String[]{"mac"}; } for(String para:paraName){ fieldValueList.add(request.getParameter(para)); } if(fieldValueList.isEmpty()){ throw new RuntimeException("@RedisLock作用于controller层方法时,生成key失败,请求中没有mac签名"); } //标示注解作用在controller成方法上 redisLockBean.setAtController(true); }else{ throw new RuntimeException("@RedisLock作用于controller层的方法时,该方法上需要使用@RequestMapping注解"); } //注解作用于非controller层方法上 }else if(method.isAnnotationPresent(RedisLock.class)){ redisLock = method.getAnnotation(RedisLock.class); //参数的索引位置 int index = redisLock.paramIndex(); String[] fieldName = redisLock.fieldName(); String[] values = getFieldValue(object[index],fieldName); //注解的参数时基本的数据类型或String,不需要传入属性名称,否则设置的属性,都必须获得该属性值 if(values==null || values.length!=fieldName.length && fieldName.length>0){ return null; } fieldValueList.addAll(Arrays.asList(values)); redisLockBean.setAtService(true); }else{ Annotation[][] annotations; annotations = method.getParameterAnnotations(); for(int i=0;i<annotations.length;i++){ for(Annotation annotation:annotations[i]){ if(annotation instanceof RedisLock){ RedisLock redisLockTmp = (RedisLock)annotation; if(redisLock==null){ redisLock = redisLockTmp; } String[] fieldName = redisLockTmp.fieldName(); String[] values = getFieldValue(object[i],fieldName); //注解的参数时基本的数据类型或String,不需要传入属性名称,否则设置的属性,都必须获得该属性值 if(values==null || values.length!=fieldName.length && fieldName.length>0){ return null; } fieldValueList.addAll(Arrays.asList(values)); redisLockBean.setAtParameter(true); } } } } //未使用注解 if(fieldValueList.isEmpty()){ return null; } //设置其它参数值 if(redisLockBean.getTimeInSecond()==0){ redisLockBean.setTimeInSecond(redisLock.timeInSecond()); } if(StringUtils.isEmpty(redisLockBean.getCodeName())){ redisLockBean.setCodeName(redisLock.codeName()); } if(StringUtils.isEmpty(redisLockBean.getCode())){ redisLockBean.setCode(redisLock.code()); } if(StringUtils.isEmpty(redisLockBean.getMsgName())){ redisLockBean.setMsgName(redisLock.msgName()); } if(StringUtils.isEmpty(redisLockBean.getMsg())){ redisLockBean.setMsg(redisLock.msg()); } Collections.sort(fieldValueList); logger.info("all value of fieldName is {}",fieldValueList); //生成key值 StringBuilder builder = new StringBuilder(); builder.append(target.getClass().getName()) .append("-") .append(method.getName()) .append("-") .append(Arrays.asList(method.getParameterTypes())) .append("-") .append(fieldValueList); String lockKey = RedisKeyConstants.REDIS_LOCK + builder.toString(); logger.info("redis lock key is :{}",builder.toString()); redisLockBean.setKey(lockKey); logger.info("redisLockBean :{}",redisLockBean.toString()); return redisLockBean; } private String[] getFieldValue(Object argObj,String...fieldName){ if(fieldName ==null || fieldName.length == 0){ return new String[]{getBaseClassValue(argObj)}; } List<String> fieldsValue = new ArrayList<String>(); for(String field:fieldName){ String value = getFieldValue(argObj,field); logger.info("value of fieldName '{}' is :{}",fieldName,value); if(value!=null){ fieldsValue.add(value); } } return fieldsValue.toArray(new String[0]); } private String getFieldValue(Object argObj,String fieldName){ if(argObj==null){ throw new RuntimeException("argObj is null,cannot get field value of fieldName"); } String value = getBaseClassValue(argObj); if(!StringUtils.isEmpty(value)){ return value; } String methodName = getGetMethodValueByFieldName(fieldName); Object result = null; try { Method method = argObj.getClass().getMethod(methodName); result = method.invoke(argObj); } catch (NoSuchMethodException e) { logger.error("method {} without parameter is not exists!",methodName); e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (IllegalAccessException e) { logger.error("method {} without parameter is not public!",methodName); e.printStackTrace(); } catch (IllegalArgumentException e) { logger.error("method {} has parameter!",methodName); e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } if(result==null){ logger.warn("method {} does not have returnValue",methodName); return null; } return getBaseClassValue(result); } private String getBaseClassValue(Object object){ if(object==null){ throw new RuntimeException("argObj is null,cannot get field value "); } if(object instanceof String){ return object.toString(); } if(object instanceof Integer){ int i = (Integer)object; //剔除成员变量的默认值 if(i!=0){ return i+""; } } if(object instanceof Long){ long i = (Long)object; if(i!=0){ return i+""; } } return null; } private String getGetMethodValueByFieldName(String fieldName){ return getMethodNameByFieldNameAndPrefix("get",fieldName); } private String getSetMethodNameByFieldName(String fieldName){ return getMethodNameByFieldNameAndPrefix("set",fieldName); } private String getMethodNameByFieldNameAndPrefix(String prefix,String fieldName){ if(StringUtils.isEmpty(fieldName)){ throw new RuntimeException("cannot get Get method by null or length is 0"); } if(StringUtils.isEmpty(prefix)){ throw new RuntimeException("cannot get Get method by null without prefix"); } String getMethodName = prefix+fieldName.substring(0, 1).toUpperCase(); //fieldName 的长度大于一时,索引大于一的字符不改变大小写 if(fieldName.length()>1){ getMethodName = getMethodName + fieldName.substring(1); } return getMethodName; } private String response(String type, Object obj) { if (XML_TYPE.equalsIgnoreCase(type)) { String ret = OxmHelper.marshal(obj); logger.info("response:{}",ret); return ret; } if (JSON_TYPE.equalsIgnoreCase(type)) { String ret = JSONObject.toJSONString(obj); logger.info("response:{}",ret); return ret; } return ILLEGAL_TYPE + ":" + type; } }
package com.slp.service.impl; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.SessionCallback; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import com.cul.culsite.service.RedisService; @Service public class RedisServiceImpl implements RedisService { private static final Logger logger = LoggerFactory.getLogger(RedisServiceImpl.class); @Autowired private RedisTemplate<String, String> redisTemplate; /** * 获取redis值 * * @param key * 键 * @return 值 */ public String get(String key) { Assert.hasText(key, "redis get key cannot null"); return redisTemplate.opsForValue().get(key); } /** * 删除redis键 * * @param key * 键 */ @Override public void delete(String key) { Assert.hasText(key, "redis delete key cannot null"); redisTemplate.delete(key); } /** * 设置redis值 * * @param key * 键 * @param value * 值 * @param time * 时间(分钟) 如果小于0 默认为1分钟 */ @Override public boolean setIfAbsent(final String key,final String value,int time) { Assert.hasText(key, "redis set key cannot null"); Assert.hasText(value, "redis set value cannot null"); if(time<=0){ time = 1; } final int timeInSecond = time; try{ @SuppressWarnings("unchecked") Object isSetSuccess = redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations arg0) throws DataAccessException { try{ //开始事务 List<Object> result=new ArrayList<Object>(); arg0.multi(); arg0.opsForValue().setIfAbsent(key, value); // arg0.expireAt(key,DateUtils.addSeconds(new Date(),timeInSecond)); arg0.expire(key, timeInSecond, TimeUnit.SECONDS); //提交事务 result= arg0.exec(); logger.info("redis mutil for get lock result is :{}",result); //执行了两次redis操作,应该有两个返回值,否则防止key永久有效,执行删除操作 if(result == null||result.size()!=2){ redisTemplate.delete(key); return false; } //获取加锁操作的返回结果 boolean setIfAbsentResult = false; if(result.get(0) instanceof Boolean){ setIfAbsentResult =(Boolean)result.get(0); } //获取设置key有效时间返回结果 boolean expireAtResult = false; if(result.get(1) instanceof Boolean){ expireAtResult = (Boolean)result.get(1); } if(setIfAbsentResult&&expireAtResult){ logger.info("加锁成功......."); return true; } }catch(Exception e){ e.printStackTrace(); } return false; } }); if(isSetSuccess instanceof Boolean){ return (Boolean) isSetSuccess; } return false; }catch(Exception e){ e.printStackTrace(); return false; } } @Override public Set<String> keys(String keyPattern) { Assert.hasText(keyPattern, "keys pattern is null"); return redisTemplate.keys(keyPattern); } @Override public long incr(String key) { Assert.hasText(key, "key is null"); return redisTemplate.opsForValue().increment(key, 1L); } @Override public long decr(String key) { Assert.hasText(key, "key is null"); return redisTemplate.opsForValue().increment(key, -1L); } @Override public void set(String key, String value) { Assert.hasText(key, "key is null"); Assert.hasText(value, "value is null"); redisTemplate.opsForValue().set(key, value); } @Override public boolean set(final String key, final long value, final int timeInSecond) { Assert.hasText(key, "key is null"); Assert.hasText(value + "", "value is null"); Assert.hasText(timeInSecond + "", "timeInSecond is null"); try{ @SuppressWarnings("unchecked") Object isSetSuccess = redisTemplate.execute(new SessionCallback() { @Override public Object execute(RedisOperations arg0) throws DataAccessException { try{ //开始事务 List<Object> result=new ArrayList<Object>(); arg0.multi(); arg0.opsForValue().increment(key, value); arg0.expire(key, timeInSecond, TimeUnit.SECONDS); //提交事务 result= arg0.exec(); logger.info("result of redis set long value is :{}",result); //执行了两次redis操作,应该有两个返回值,否则防止key永久有效,执行删除操作 if(result == null || result.size() != 2){ redisTemplate.opsForValue().increment(key, (0 - value)); return false; } //获取加锁操作的返回结果 long incrementResult = 0; if(result.get(0) instanceof Long){ incrementResult =(Long)result.get(0); } //获取设置key有效时间返回结果 boolean expireAtResult = false; if(result.get(1) instanceof Boolean){ expireAtResult = (Boolean)result.get(1); } if((incrementResult == value) && expireAtResult){ return true; } }catch(Exception e){ e.printStackTrace(); } redisTemplate.opsForValue().increment(key, (0 - value)); return false; } }); if(isSetSuccess instanceof Boolean){ return (Boolean) isSetSuccess; } return false; }catch(Exception e){ e.printStackTrace(); return false; } } public Long getLong(String key) { try{ Set<String> keys = redisTemplate.keys(key); //key指定的数据不存在 if (keys == null || keys.isEmpty()) { return null; } return redisTemplate.opsForValue().increment(key, 0); } catch (DataAccessException e) { logger.info("error :{}", e); logger.info("{}指定的数据不是数值类型", key); throw new RuntimeException(key + "指定的数据不是数值类型"); } } public Long getLongNoKeys(String key) { try { long keys = redisTemplate.opsForValue().increment(key, 0); return keys; } catch (DataAccessException e) { logger.info("error :{}", e); logger.info("{}指定的数据不是数值类型", key); throw new RuntimeException(key + "指定的数据不是数值类型"); } } /** * 删除set集合中的对象 * @param key * @param value */ @Override public void srem(String key, String value) { redisTemplate.boundSetOps(key).remove(value); } }
1 /** 2 * 3 * @Description: 4 * @param @param request 5 * @param @return 6 * @param @throws Exception 7 * @return String 8 * @throws 9 * @author liping.sang 10 * @date 2017-8-8 11 */ 12 @RedisLock(fieldName={"reqNo"},timeInSecond=3) 13 @RequestMapping(method = { RequestMethod.GET, RequestMethod.POST }, value = "/test2") 14 @ResponseBody 15 public String test2(HttpServletRequest request) 16 throws Exception {
原文链接:https://www.cnblogs.com/chenGG/p/10875302.html
如有疑问请与原作者联系
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
- redis缓存 2020-06-12
- Spring系列.ApplicationContext接口 2020-06-11
- Spring Boot 2.3.0 新特性Redis 拓扑动态感应 2020-06-11
- 作为一个面试官,我想问问你Redis分布式锁怎么搞? 2020-06-10
- logstash系列-入门整理 2020-06-10
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash