编码的那么点事儿

编码的那么点事儿

    在这篇文章中很少会去详细、硬性的去要求要如何写代码,更多的是提出一种编码的时候需要注意的一些情况,和一些更安全,更高效的编码思路。但是在现有框架下,也会对Service,DAO类有一些硬性的命名要求。
    在redis的使用场景上也会有一些介绍,redis做为一个分布式NoSQL数据库,也是现在比较流行的缓存方案,它可以解决不少问题,但是也有一些问题我们需要去避免。
    希望阅读本篇文章,能让你对编码有新的认识,而不再是copy 和paste。

基础

  1. 命名和术语

    • 使用英文单词命名,不用汉语拼音,达到顾名思义
    • 类名首字母大写,后面单词的首字母大写,其余字母小写。其次类名要表达类的作用,如Service接口,就像UserService,
      如Service实现类,就像UserServiceImpl,如Dao类,就像UserDao。
    public class MidSchoolClass {
        private class Group {
    
        }
    
        private static class Member {
    
        }
    
    }
    
    public interface UserService{
    
    }
    
    public class UserServiceImpl extends UserService{
    
    }
    
    public interface UserDao{
    
    }
    • 静态方法,成员方法和成员字段都使用驼峰的方式命名。前面是动词,后面是名词,如新增会员:addMember(),如单元测试:testAddMember()。
    private Member oldMember = new Member();
    private int countMembers = 0;
    
    /** 
    * 新增会员
    * @param newMember 新会员
    */
    public void addMember(Member newMember){
    //TODO ....
    }
    
    /**
    * (意思你懂的)
    * @param obj 对象
    */
    public void fuckTheWorld(Object obj){
    //TODO ...
    }
    • 静态字段或常量使用大写,单词之间使用下划线分开。
    public static final int MEMBER_STATUS_DELETED = 0;
    private static Member MIDDLE_SCHOOL_TEACHER = new Member();
  2. 注释

    • 类的注释需要规范:作者、时间、功能介绍等;如果比较复杂的,还需要有示例代码。
    • 方法级别的注释需要对方法的功能做详细介绍,对方法的参数做解释,介绍返回的内容。如果是实现接口方法或抽象方法可以不用写注释,但重写父类的方法需要写注释。
    • 复杂的逻辑部分需要添加注释。
    • 长方法在多个子代码块添加注释。
    • 对常量字段加注释描述。
    • 对类或方法进行修改,需要对注释进行相应的修改,并加上@author 作者。

    看个例子

    import java.lang.annotation.Annotation;
    /**
    * 通过{@code AnnotationHandlerInterceptor}对http请求按照Handler标注的Annotation调用指定的拦截器
    * 拦截器实现这个接口,可以将拦截到的Handler的Controller类和方法上标注的运行时Annotation注入
    * <code>
    * <mvc:interceptors>
    *      <!-- 验证是否登录 -->
    *      <mvc:interceptor>
    *          <mvc:mapping path="/**" />
    *          <bean class="com.zzh.college.interceptors.AnnotationHandlerInterceptor">
    *              <property name="annotationTarget">
    *                  <value>TYPE_AND_METHOD</value>
    *              </property>
    *              <property name="annotationClass"
    *                  value="com.zzh.college.request.LoginIntercept" />
    *              <property name="handlerInterceptors">
    *                  <array>
    *                      <bean class="com.zzh.college.interceptors.LoginInterceptor"></bean>
    *                  </array>
    *              </property>
    *          </bean>
    *      </mvc:interceptor>
    * </code>
    * <code>
    * public class LoginInterceptor extends HandlerInterceptorAdapter implements AnnotationInterceptorAware {
    * 
    *  private LoginIntercept loginIntercept;
    * public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    *          throws Exception {
    *      if (loginIntercept.intercept() == InterceptAccess.LOGIN) {
    *          return checkLogin(request, response, handler);
    *      }
    *      return super.preHandle(request, response, handler);
    *  }
    *  
    *      @Override
    *  public void setAnnotation(Annotation annotation) {
    *      loginIntercept = (LoginIntercept) annotation;
    *  }
    * 
    * }
    * </code>
    * 
    * TODO <b>之后会加入泛型扩展</b>
    * @author [email protected]
    * @date 2016.11.16 16:00:00
    *
    */
    public interface AnnotationInterceptorAware {
    
    /**
     * Annotation的注入方法
     * @param annotation
     */
    void setAnnotation(Annotation annotation);
    }

代码格式化

所有代码提交至SVN,GIT前,先进行Format

项目小组使用统一格式化风格对代码进行格式化,这样便于小组成员间相互的code review;在代码更新、提交、合并的时候能够清晰地与老版本代码对比,查看所修改的代码。
举几个例子

  • 某个小组成员在提交之前将代码格式化了,到时整个类的风格都变了,在代码的版本控制系统上与上个版本的代码进行对比,光想想就脑阔疼了吧。
  • 某个小组成员写了一行非常长的代码
//比如com.zzh.platform.vendor.service.impl.ProductServiceImpl#insertProduct()方法
public class ProductServiceImpl implements ProductService {
    //这是一行代码
    public void insertProduct(ProductInfo prodcutInfo,List<ProductCompanyInfo> productCompanyInfos, long[] categoryIds,List<DictionaryInfo> serviceScopes) throws InterruptedException {
        //TODO ...
    }
}

如果这个方法的throws的异常类改变了,在svn上进行对比时分成两个页面显示,要看到这个变动要移动鼠标,更多人可能会略过;如果有很多行这样的长代码改动了,要看其中的改变,也是非常痛苦吧。

  • 在写代码的时候还要关心代码的格式,要让它便于阅读,累不累呀?而且其他人还不一定欣赏你的代码风格。

项目中管理ddl/sql脚本

新建一个source folder,用来管理每个版本的修改调整的数据库脚本文件。

这里简单的介绍一下,我以前在项目中来管理数据库脚本的方法。
目录结构如下:

src/main/sql
1.0.0

create_schema.sql
1.0.1
modify_table.sql

每个版本新建一个文件夹,在文件夹中新建sql脚本文件,文件命名也代表着文件内容。
这样就能够清淅的看到每个版本中,数据库的变动。我们开发相互之间可以review,在版本发布的时候,也可以很快的提交给测试和dba。

代码书写

  1. 避免基本性的一些bug

    • 基本类型的包装类型在参与运算时,要做null值校验,否则可能出现空指针异常。
    • 基本类型和包装类型的转换需要特别注意,如果包装类型为null,这时将包装类型转换为基本类型就会抛出空指针异常。尤其是在方法传参时,包装类型和基本类型会自动转换,这时特别需要注意。
    • 使用New 生成的两个基本类型的包装类必然不同,通过包装类的ValueOf生成的包装类实例可以显著提高空间和时间性能。
    • 判断对象是否相等的时候使用equals方法,避免使用“==”产生非预期结果。
  2. 无特殊要求,优先使用基本类型。因为包装类被创建后,会在堆中分配内存,非静态的基本类型的数据是存在方法栈中,使用的是栈的内存,栈的操作速度比堆快很多。

  3. 常量的定义也可以分域,其实有些常量没有必要放到公共的常量对象中,它可以放很多“地方”,选择一种让小组成员很快找到的方式就好。

    • 可以放到接口中,不必放到实现类中。
    • 也可以在某一个模块的包下创建一个常量对象
    • 也可以在模块中声明一个包专门存放enum对象,这些enum对象都是对常量的一种表达
    • 有一种设计叫领域驱动,它有领域对象,领域服务等,领域对象也可以存放常量,而且是最容易查找的。但是在这里仅仅只是介绍一下有这种设计,目前不推荐使用这种方式。

    在公服平台,有些常直接定义在zzh-common项目下的某个类中,这样其实很不可取,因为zzh-common是一个工具包,更多的时候小组成员使用里面封装好的方法就行,而且大部分常量只跟某个特写的项目有关,甚至只跟项目中的某个模块有关,在开发的时间,肯定只在模块中修改代码更快,而且只对依赖于该应用的项目开放,这样做之后,zzh-common这个项目,只需要提供jar就行了,也不用去改动zzh-common下的代码,这个项目更稳定。

  4. 多次使用的相同变量最好归纳成常量,多处使用的相同值的变量应该尽量归纳为一个常量,方便日后的维护。

  5. 尽量少的在循环中执行方法调用
    尽量在循环中少做一些可避免的方法调用,这样可以节省方法栈的创建。例如:
    forint i=0;i<list.size();i++){
        System.out.println(i);
    }
    //可以修改为:
    forint i=0,size=list.size();i<size;i++){
    System.out.println(i);
    }
  1. String,StringBuffer和StringBuilder
    这个问题比较常见。在进行字符串拼接处理的时候,String通常会产生多个对象,而且将多个值缓存到常量池中。例如:

    String a=“a”;
    String b=“b”;
    a=a+b;

    这种情况下jvm会产生“a”,“b”,“ab”三个对象,而且字符串拼接的性能也很低。因此通常需要做字符串处理的时候尽量采用StringBuffer和StringBuilder来,而StringBuffer是使用synchronized关键字实现的线程安全的类,看到这是不是都在想:那应该使用StringBuffer啦?其实不然,大部分对字符的操作基本都是封闭在线程内的,根本没必要去同步,所以我们使用StringBuilder的机会会更多。

  2. 在finally块中对资源进行释放
    典型的场景是使用io流的时候,不论是否出现异常最后都应该在finally中对流进行关闭。

    public void readFile(File file){
        InputStream in = null;
        try{
            in = new FileInputStream(file);
            //reading ....
        }catch(IOException e){
            LOG.error(e.getMessage(), e);
        }finally{
            if(in!=null){
                try{
                    in.close();
                }catch(IOException e){
    
                }
            }
        }
    }

    还有是使用java.util.concurrent.Lock的锁的时候常用来做释放锁的操作。

    private ReentrantLock lock = new ReentrantLock();
    public void modifySomeOn(){
        lock.lock();
        try{
            // to do some things...
        }cache(Exception e){
            LOG.error(e.getMessage(), e);
        }finally{
            lock.unlock();
        }
    }
  3. 了解apache-commons,使用其提供的封装好的类。

    对String的一些常用操作,调用StringUtils中的方法去执行,如果没有的方法,可以自己再去封装。

    // String equals
    String str = "";
    String str2 = " ";
    boolean isEquals = str.equals(str2);
    boolean isEquals2 = StringUtils.equals(str, "abc");
    
    // String empty
    boolean isEmpty = str==null || "".equals(str); //return true
    boolean isEmpty2 = StringUtils.isEmpty(str);//return true
    boolean isEmpty2 = StringUtils.isBlank(str2); //return true
    
    //String strip
    StringUtils.strip(null, *); // null
    StringUtils.strip("", *); // ""
    StringUtils.strip("abc", null); // "abc"
    StringUtils.strip("  abc", null); // "abc"
    StringUtils.strip("abc  ", null); // "abc"
    StringUtils.strip(" abc ", null); // "abc"
    StringUtils.strip("  abcyx", "xyz"); // "  abc"
    StringUtils.strip("aefabcdfea", "ef"); //会返回什么结果?
    
    //去除全角的空格
    StringUtils.strip(" jf  ");//"jf"

    对日期的常用操作可以先去了解DateUtils

    Date date = new Date();
    DateUtils.addYears(date, 1);
    DateUtils.addMonths(date, 1);
    DateUtils.addWeeks(date, 1);
    DateUtils.addDays(date, 1);
    DateUtils.addHours(date, 1);
    DateUtils.addMinutes(date, 1);
    DateUtils.addSeconds(date, 1);
    DateUtils.addMilliseconds(date, 1);
    DateUtils.add(date, Calendar.HOUR, 1);
    DateUtils.setYears(date, 1);
    DateUtils.setMonths(date, 1);
    DateUtils.setWeeks(date, 1);
    DateUtils.setDays(date, 1);
    DateUtils.setHours(date, 1);
    DateUtils.setMinutes(date, 1);
    DateUtils.setSeconds(date, 1);
    DateUtils.setMilliseconds(date, 1);
    DateUtils.set(date, Calendar.HOUR, 1);

    如果没有的方法,可以去建个DateUtils,继承org.apache.commons.lang.StringUtils.DateUtils再去扩展。

    对File的基本操作可以使用commons-io.jar中的org.apache.commons.io.FileUils。

    // get a file
    File file = FileUtils.getFile("d:/a", "b", "c", "d.txt");//file ==> d:/a/b/c/d.txt
    
    //copy file
    File src = new File("d:/a/b.txt");
    File target = new File("e:/a/b.txt");
    FileUtils.copyFile(src, target);
    
    //copy directory
    File srcDir = new File("d:/a");
    File targetDir = new File("d:/b");
    FileUtils.copyDirectory(srcDir, targetDir);
    
    //get input or output stream
    File file = new File("d:/a.txt");
    OutputStream out = FileUtils.openOutputStream(file);
    InputStream in = FileUtils.openInputStream(file);
    
    //read file string content
    File file = new File("d:/a.txt");
    String content = FileUtils.readFileToString(file, Charset.forName("UTF-8"));
    
    //write content to a file
    File file = new File("d:/a.txt");
    FileUtils.writeStringToFile(file, "content", Charset.forName("UTF-8"));
    FileUtils.writeByteArrayToFile(file, "content2".getBytes(Charset.forName("UTF-8")));

    如果没有的方法,可以新建个FileUtils,继承org.apache.commons.io.FileUtils再去扩展。
    还有apache-commons项目下还有很其它的工具类,都可以去了解,并去封装扩展,比如对io进行操作的org.apache.commons.io.IOUtils, 对Bean属性进行操作的org.apache.commons.beanutils.PropertyUtils,对Bean进行操作的org.apache.commons.beanutils.BeanUtils和org.springframework.beans.BeanUtils,对基本类型进行转换的org.apache.commons.beanutils.ConvertUtils等等,这里指出这些,只是提出这样这种思想:使用一些开源组织封装好的工具类去做一些事,不够的,可以再去扩展封装。

  4. 注意数据边界以及数据安全的校验
    了解数据边界,对于编码的效率会有很大的提高,为什么呢?因为你不用思考一些判断我该放在哪儿呢?这里是否需要进行判断呢?那么了解了数据边界,就知道该在那里进行判断,编码速度提高了,代码性能提高了,程序的稳定性也提高了。可是数据边界怎么去了解?我们将程序分开应用层,服务层,数据层,就是为了解决这些问题,应用层做为跟前端接触的那一层,有些null判断直接在应用层判断了,服务层根据应用层传递过来的数据再写业务,将数据提交给数据层持久化。这样说可能太抽象,还是以例子阐述吧。

    新增用户

    1. 应用层的UserController接收前端的请求,对数据进行验证。
    @Controller
    @RequestMapping("/user")
    public UserController {
    
    private static Logger LOG =     LoggerFactory.getLogger(UserController.class);
    
    @Autowired
    private UserService userService;
    
    /** 
     * 添加一个会员
     * @param username
     * @param password
     * @param rePassword
     * @return apiresult json
     */ 
    @RequestMapper("addMember")
    @ResponseBody
    public ApiResult addMember(String username, String password, String rePassword){
        //在handler方法中进行基本的验证
        if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){
            return new ApiResult(-1, "用户名和密码不能为空!");
        }else if(StringUtils.equals(password, rePassword)){
            return new ApiResult(-1, "两次密码输入须一致!");
        }
    
        try{
            Member member = userService.addMember(username, password);
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("id", member.getId());
            map.put("username", member.getUsername());
            return new ApiResult(0, map);
        }catch(Exception e){
            LOG.error(e.getMessage(), e);
            return new ApiResult(-1, "添加会员失败,请重试!");
        }
    }
    }
    
    
    
    
    
    public class UserService {
    
    /** 
     * 添加一个会员
     * @param username
     * @param password
     * @return 
     * @throws RuntimeException 如果添加失败,将会抛出异常
     */ 
    public Member addMember(String username, String password) throws RuntimeException;
    
    }
    
    
    
    @Service
    public class UserServiceImpl implements UserService {
    
     @Autowired
     private UserDao userDao;
    
     @Override
     public Member addMember(String username, String password) throws RuntimeException {
        if(StringUtils.length(username)<1 || Stringutils.length(username)>30){
            throw new RuntimeException("用户名的长度必须在1-30位之间");
        }else if(StringUtils.length(password)<6){
            throw new RuntimeException("密码不能小于6位");
        } 
        Member member = new Member();
        member.setUsername(username);
        member.setPassword(StringUtils.encrypt(password));
        userDao.insert(member);
     }
    }

    这样的代码是不是很清淅呢,因为判断边界很清楚,应用层做一些基础的判断,保证数据的正确性提交给服务层,服务层做一些业务的判断,提交给数据层,这样就是避免了在写代码的时候,还要去思考,是否要做非空判断,数据长度的判断啊等等,也不会出现应用层写一遍判断,服务层再去写一遍判断。

  5. 编写网络io操作的时候,一定要考虑到响应速度,设置合理的超时时间。
    如查在编码的时候没有考虑到响应速度,设置超时时间,会产生哪些影响呢?
    发生网络抖动的时候,请求一直得不到目标服务器的响应,会阻塞在那,那么会这么几情况

    • 前端也会相应等待。
    • 线程会等待。
    • 如果持有锁,锁不会被释放。
    • 如果有事务,数据库连接不会被释放。
  6. 消耗cpu和io资源的数据,要考虑使用缓存。
    缓存有一句话是这样来描述它的,“空间换时间”,所以缓存的优势是快速读写,但是它会消耗内存,所以缓存的数据,需要分析其热点数据,并且是消耗cpu和io资源的数据。
    在java中可以使用SoftReference或WeakReference来实现内存敏感的高速缓存。使用软引用能防止内存泄露,增强程序的健壮性。
    对于引用有四种:

    • 强引用

      强引用的“强”是取决于它如何处理与GC的关系的:它是无论如何都不会被回收的。

    • 软引用

      软引用(SoftReference)则类似于可有可无的东西。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

    • 弱引用

      弱引用(WeakReference)也类似于可有可无的东西。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,从gcroot处没有查找其它的引用,不管当前内存空间足够与否,都会回收它的内存。

    • 虚引用
      虚引用(PhantomReference)这个类型的引用无可奉告,因为我也不知道。 - -

    看代码和输出吧。

        SoftReference<Cache> soft = new SoftReference<Cache>(new Cache(1));
        WeakReference<Cache> weak = new WeakReference<Cache>(new Cache(3));
        PhantomReference<Cache> phantom = new PhantomReference<Cache>(new Cache(4), queue);
        System.out.println("soft --> " + soft.get());//soft --> 1
        System.out.println("weak --> " + weak.get());//weak --> 3
        System.out.println("phantom --> " + phantom.get());//phantom --> null
    
        System.gc();
        try {
            Thread.sleep(30 * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        System.out.println("===after gc==");//===after gc==
        System.out.println("soft --> " + soft.get());//soft --> 1
        System.out.println("weak --> " + weak.get());//weak --> null
        System.out.println("phantom --> " + phantom.get());//phantom --> null
        SoftReference<Cache> soft = new SoftReference<Cache>(new Cache(1));
        Cache cache = new Cache(3);
        WeakReference<Cache> weak = new WeakReference<Cache>(cache);
        PhantomReference<Cache> phantom = new PhantomReference<Cache>(new Cache(4), queue);
        System.out.println("soft --> " + soft.get());//soft --> 1
        System.out.println("weak --> " + weak.get());//weak --> 3
        System.out.println("phantom --> " + phantom.get());//phantom --> null
    
        System.gc();
        try {
            Thread.sleep(30 * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        System.out.println("===after gc==");//===after gc==
        System.out.println("soft --> " + soft.get());//soft --> 1
        System.out.println("weak --> " + weak.get());//weak --> 3
        System.out.println("phantom --> " + phantom.get());//phantom --> null

    可以看到上面两段代码中WeakReference的引用在gc前后输出是不一样的,这是因为在第二段代码中,WeakReference引用了变量cache,cache还存在方法栈被引用到,所以还没有被gc回收。

    上面只是介绍了避免因缓存而导致jvm内存溢出的技术,还可以通过算法对缓存进行管理,比如LRU算法,FIFO算法等,感兴趣的猿可以了解了解,然后结合下面介绍线程安全的节章设计一个线程安全的缓存工具来。

  7. 线程安全
    线程安全?好高大上酷拽吊!但是其实一开始每个人写的第一个程序就是线程安全的程序,输出“hello world”,没错,这就是一个线程安全的程序,它只有一个main线程,没有其它线程跟main线程发生竟争,它能保证输出”hello world”,它就是线程安全的。
    线程和并发几乎总是会在一起被谈论,这是必然的,因为进程实操作系统的并发,jvm线程实现了java程序的并发,所以呢,想写出好的并发程序,那么你必须了解线程,以及它的同步机制,大家可以单独去了解,这里只讲讲编写线程安全的类的思路,也会带一点点并发方面的考虑。

    • 一个只有不可变基础类型属性(字段)的类,它一定是线程安全的。
    public class ApiRequest{
        private final String version;
        private final long userid;
    
        public ApiRequest(String version, long userId){
            this.version = version;
            this.userId = userId;
        }
    
        public String getVersion(){
            return version;
        }
    
        public long getUserId(){
            return version;
        }
    }

    需要注意的是,一个包含不可变的引用类型属性的对象,并不能保证足够的安全,除非那个引用类型也只包含不可变的基础类型的属性。看下面的代码

    public class ApiRequest{
        private final String version;
        private final long userid;
        private final LoginUser user;
    
        public ApiRequest(String version, long userId, LoginUser user){
            this.version = version;
            this.userId = userId;
            this.user = user;
        }
    
        //getter methods ....
    }
    
    public class LoginUser {
        private String username;
        private String password;
    
        public LoginUser(String username, String password){
            this.username = username;
            this.password = password;
        }
    
        //getter/setter methods ....
    }

    上面代码中ApiRequest对象并不是线程安全的,为什么呢?因为LoginUser不是线程安全的。如果LoginUser修改按下面的代码那样修改一下就是了。

    public class LoginUser {
        private final String username;
        private final String password;
    
        public LoginUser(String username, String password){
            this.username = username;
            this.password = password;
        }
    
        //getter methods ....
    }
    • 封闭在线程内的对象,并且没有对其它线程发布的对象,它一定是线程安全的。
    @Controller
    @RequestMapping("user")
    public class UserController{
    
        @Autowired
        private UserService userService;
    
        public ApiResult addMember(HttpServletRequest request){
            Member member = new Member();
            member.setUsername(request.getParameter("username"));
            member.setAge(request.getParameter("age"));
    
            //只要后面的代码不将member对象发布到其它线程,那么member对象就是一个线程安全的对象。
            userService.addMember(member);
    
        }
    }

    因为一个线程会被分配一个栈空间,比如member对象,没有对这个线程之外的范围进行发布,也就是在这个线程的栈空间之外没有这个对象的引用,那么其它线程自然没办去对它进行访问。把一个变量发布出去,最直接的方法就是赋予静态域,或发布到静态域对象的引用中。

    • 使用ThreadLocal对象,将对象封闭到线程内,从而实现程序安全。
    public class AInterceptor extends HandlerInterceptorAdapter {
        @Autowired
        private MemberService memberService;
        private ThreadLocal<Member> memberLocal = new ThreadLocal(); 
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {
            long memberId = StringUtils.defaultIfEmpty(request.getParameter("memberId"), "0");
            Member member = memberService.findById(memberId);
            //这时就可以通过memberLocal对外发布member对象了。
            memberLocal.set(member);
            return super.preHandle(request, response, handler);
        }
    
        /** 
         * 这就是一个线程安全的方法
         * @return
         */
        public Member getCurrentMember (){
            return memberLocal.get();
        }
    
        @Override
        public void afterCompletion(
                HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
                memberLocal.remove();
        }
    }
    

    ThreadLocal为什么能实现线程安全呢,因它他是个Map,Key就是当前线程 –> Thread.currentThread(),有兴趣的同学可以去了解一下。

    • 使用锁将需要同步的代码变成串行执行。
      java中锁有两种,synchronized和ReentrantLock。这两种锁最大的区别不是性能上的区别,而且是它们底层实现上的区别。

      1. synchronized是jvm级别的锁,由jvm管理,它锁的是对象,如果已有其它线程持有锁,那么当前线程就会一直阻塞等待,至到持有锁的线程执行的代码退出synchronized代码块释放掉这个对象的锁,比如:

            public class Test(){
                private byte[] lock = new byte[0];
                public static synchronized void staticLock(){
                    //这个方法锁的是Test.class
                }
        
                publi synchronized void lockThis(){
                    //这个方法锁的是this
                }
        
                publc void lockObj(){
                    synchronized(lock){
                    //这段代码锁的是数组对象lock
                    }
                }
            }
      2. ReentrantLock是通过无阻塞算法实现,使用的技术是cas(compare and set,有兴趣可以去看看AbstractQueuedSynchronizer.acquireQueued(final Node node, int arg)的代码,下面会有介绍到)。

        public class Test{
        private ReentrantLock lock = new ReentrantLock();
        
        private void lock(){
            if(lock.tryLock(200, TimeUnit.MILLISECONDS)){
                try{
                    // to do some things ...
                }catch(Exception e){
        
                }finally{
                    lock.unlock();
                }
            }
        }
        }
        
      3. 两种的性能对比
        在jdk1.6以前,是ReentrantLock性能要占优势,但是在jdk1.6中,sun对synchronized进行了优化,性能上已经渐渐赶上来了,而且jdk在以后可能会继续对synchronized进行优化。其实大部份情况下,synchronized已经能够满足大部分使用场景。如果需要使用更灵活的并发控制(ReentrantLock提供了newCondition()方法创建一个Condition对象),使用ReentrantLock会更合适些。但是在并发非常激烈的时候,但是又可以不用考虑超时的场景,那更合适使用synchronied,为什么?大家想像一下非阻塞算法的场景,再结合对线程的了解,就可以知道了。

      4. Condition提供的方法

        //阻塞线程,
        await()
        //阻塞线程,如果超时才退出该方法,返回false,否则true
        boolean awaitUntil(Date deadline)

        //阻塞线程,如果超时才退出该方法,返回false,否则true
        await(long time, TimeUnit unit)

        //唤醒单个线程
        signal()

        //唤醒所有线程
        signalAll()

    • 使用cas非阻塞的算法提供线程安全。
      看看AbstractQueuedSynchronizer中的自旋获取锁的那一段代码。
      判断当前节点的前驱节点。需要获取当前节点的前驱节点的状态,当前驱节点是头结点并且当前节点(线程)能够获取状态(tryAcquire方法成功),代表该当前节点占有锁,如果满足上述条件,那么代表能够占有锁,根据节点对锁占有的含义,设置头结点为当前节点(setHead)。如果没有满足上述条件,调用parkAndCheckInterrupt方法使得当前线程disabled,直到unpark调用或者Thread的interrupt调用,然后重新轮训去尝试上述操作。

          final boolean acquireQueued(final Node node, int arg) {
          boolean failed = true;
          try {
              boolean interrupted = false;
              for (;;) {
                  final Node p = node.predecessor();
                  if (p == head && tryAcquire(arg)) {
                      setHead(node);
                      p.next = null; // help GC
                      failed = false;
                      return interrupted;
                  }
                  if (shouldParkAfterFailedAcquire(p, node) &&
                      parkAndCheckInterrupt())
                      interrupted = true;
              }
          } finally {
              if (failed)
                  cancelAcquire(node);
          }
      }
      
      private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
          int ws = pred.waitStatus;
          if (ws == Node.SIGNAL)
              /*
               * This node has already set status asking a release
               * to signal it, so it can safely park.
               */
              return true;
          if (ws > 0) {
              /*
               * Predecessor was cancelled. Skip over predecessors and
               * indicate retry.
               */
              do {
                  node.prev = pred = pred.prev;
              } while (pred.waitStatus > 0);
              pred.next = node;
          } else {
              /*
               * waitStatus must be 0 or PROPAGATE.  Indicate that we
               * need a signal, but don't park yet.  Caller will need to
               * retry to make sure it cannot acquire before parking.
               */
              compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
          }
          return false;
      }
    • 使用已经有的线程安全类去实现同步,从而提供线程安全。
      总结来说:线程安全就是供数据访问保护,出现多个线程先后更改数据造成所得到的数据不会出现数据不一致或数据污染,线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
      在某些并发场景下,可以使用已经有的线程安全的类
      concurrent包有:ConcurrentHashMap, CopyOnWriteArrayList,CopyOnWriteArraySet,CountDownLatch,AtomicInteger,AtomicIntegerArray,AtomicReference,AtomicReferenceArray等等。

      synchronized:
      Collections.synchronizedList(list);
      Collections.synchronizedCollection(c);
      Collections.synchronizedMap(m);
      Collections.synchronizedSet(s);
      Collections.synchronizedSortedMap(m);
      Collections.synchronizedSortedSet(s);

  8. 项目配置文件规范
    配置文件一般在开发的时候也会常去修改,并且随着项目的推进,配置文件也可能会越来越多,当配置文件多的时候,一眼望去,眼睛都花了,所以就需要对配置文件进行合理的管理。
    {profile}表示运行环境:开发环境,测试环境,预发环境,生产环境

    • 项目的properties配置文件

      1. 文件放到src/main/resources/{profile}/conf目录下
      2. 命名规则全小写字母,单词之间用中杠线(-)分开,如system-config.properties。
    • spring xml配置文件

      1. 文件到src/main/resources/spring/context目录下
      2. 命名规则全小写字母,单词之单用中杠线(-)分开,如spring-context.xml
    • hessian xml配置文件

      1. 文件放到src/main/resources/hessian目录下
      2. 命名规则全小写字母,单词之间用中杠线(-)分开,如hessian.xml,hessian-spring.xml
    • summercool-spring xml配置文件

      1. 文件放到src/main/resources/summercool/spring目录下
      2. 命名规则全小写字母,单词之间用中杠线(-)分开,user-web.xml
    • spring mvc xml配置文件

      1. 文件放src/main/resources/spring目录下
      2. 命名为spring-mvc.xml
    • log 的配置文件

      1. proper文件放在src/main/resources/{profile}目录下
      2. 如log4j.properties,log4j.xml,logback.xml
    • mybatis xml映射文件

      1. 文件放在src/main/resources/mapper目录下
      2. 命名规则全小写字母,单词之间用中杠线(-)分开,“zzh-*-mybatis-mapper.xml”,后面能够看出表名,如:zzh-demand-mybatis-mapper.xml,zzh-demand-dynaimc-mybatis-mapper.xml

      目的是能够快速查找,并且模块分开。

  9. log日志
    log日志是我们分析线上运行情况的重要依据,我们可以从日志中分析出哪些接口被调用的多,哪些接口被调用的不正常,还有出现异常时我们可以通过日志查看到当时运行方法栈信息,和出现异常的原因。当然,这些都要程序猿在编码的时候编写好,所以看看下面的编写吧。
    生产环境

    • 记录的内容
      1. 生产环境输出info,warn,error级别的日志。
      2. info 输出一些重要的信息。
      3. warn 输出警告。
      4. error 输出错误,异常。错误信息须描述清楚是什么原因。
    • log文件按小时折分。
    • log文件存放在web容器默认的logs文件夹下。

    开发环境
    开发环境相对来说要打印的日志就多了,除了上面那些内容,还有就是要打印一些debug信息,方便在开发的调度,能够快速定位问题点。

  10. 避免xss攻击
    xss翻译过来就是跨域脚本攻击,避免这种攻击主要的方法就是将html中的<|>|&|”等特殊敏感的字符改为实体引用。目前已经有代码实现了这个功能,我们只要拿来用就行了。

    1. 主要用到commons-lang3-3.1.jar这个包的org.apache.commons.lang3.StringEscapeUtils.escapeHtml4()这个方法。
      在web.xml加一个filter
    <filter>
        <filter-name>XssEscape</filter-name>
        <filter-class>cn.pconline.morden.filter.XssFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>XssEscape</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

    XssFilter 的实现方式是实现servlet的Filter接口

    package cn.pconline.morden.filter;
    
    import java.io.IOException;
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    
    public class XssFilter implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response);
        }
    
        @Override
        public void destroy() {
        }
    }

    XssHttpServletRequestWrapper 的代码

    public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper{
    
    
    
    
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    
    
    
    @Override    
    public String getHeader(String name) {    
        return StringEscapeUtils.escapeHtml4(super.getHeader(name));    
    }    
    
    @Override    
    public String getQueryString() {    
        return StringEscapeUtils.escapeHtml4(super.getQueryString());    
    }    
    
    @Override    
    public String getParameter(String name) {    
        return StringEscapeUtils.escapeHtml4(super.getParameter(name));    
    }    
    
    @Override    
    public String[] getParameterValues(String name) {    
        String[] values = super.getParameterValues(name);    
        if(values != null) {    
            int length = values.length;    
            String[] escapseValues = new String[length];    
            for(int i = 0; i < length; i++){    
                escapseValues[i] = StringEscapeUtils.escapeHtml4(values[i]);    
            }    
            return escapseValues;    
        }    
        return super.getParameterValues(name);    
    }  
    
    }

    到此为止,在输入的过滤就完成了。

    另外,有些情况不想显示过滤后内容的话,可以用StringEscapeUtils.unescapeHtml4()这个方法,把StringEscapeUtils.escapeHtml4()转义之后的字符恢复原样。

  11. 避免sql注入
    java防SQL注入的已经有了PreparedStatement预编译语,它内置了处理SQL注入的能力,只要使用它的setXXX方法传值即可,大部分的SQL注入已经挡住了, 在WEB层我们可以过滤用户的输入来防止SQL注入比如用Filter来过滤全局的表单参数。
    在web.xml中添加一个filter

    <filter>
        <filter-name>sqlFilter</filter-name>
        <filter-class>com.filter.SQLFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>sqlFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

    献上java代码
    SQLFilter

    import java.io.IOException;
    import java.util.Iterator;
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.lang3.StringUtils;
    
    /**
     * 通过Filter过滤器来防SQL注入攻击
     * 
     */
    public class SQLFilter implements Filter {
        private String[][] keyValues = { { "'", "‘" }, { ";", ";" } };
        protected FilterConfig filterConfig = null;
        /**
         * Should a character encoding specified by the client be ignored?
         */
        protected boolean ignore = true;
    
        public void init(FilterConfig config) throws ServletException {
            this.filterConfig = config;
            // key:value|key:value|key:value
            String keywords = filterConfig.getInitParameter("keywords");
            if (StringUtils.isNotEmpty(keywords)) {
                String[] arys = keywords.split("|");
                keyValues = new String[arys.length][2];
                for (int i = 0, size = arys.length; i < size; i++) {
                    String[] words = arys[i].split(":");
                    if (words.length == 2) {
                        keyValues[i] = words;
                    }
                }
            }
        }
    
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse res = (HttpServletResponse) response;
            chain.doFilter(new SqlHttpServletRequestWrapper((HttpServletRequest) request, keyValues), response);
        }
    }

    SqlHttpServletRequestWrapper


import java.util.Collections;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class SqlHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private Map<String, String[]> parameters;
    private String[][] keyValues;

    public SqlHttpServletRequestWrapper(HttpServletRequest request, String[][] keyValues) {
        super(request);
        this.keyValues = keyValues;
        initParameters();
    }

    @Override
    public String getParameter(String name) {
        String[] strs = parameters.get(name);
        if (strs == null || strs.length == 0) {
            return null;
        }
        return strs[0];
    }

    @Override
    public Map getParameterMap() {
        return Collections.unmodifiableMap(this.parameters);
    }

    @Override
    public String[] getParameterValues(String name) {
        return this.parameters.get(name);
    }

    private void initParameters(){
        Map<String, String[]> params = (Map<String, String[]>) super.getParameterMap();
        Iterator<String> iter = params.keySet().iterator();
        while (iter.hasNext()) {
            String key = iter.next();
            String[] valueAry = (String[]) params.get(key);
            String[] values = new String[valueAry.length];
            for (int i = 0; i < valueAry.length; i++) {
                values[i] = replaceSqlKey(valueAry[i]);
            }
            parameters.put(key, values);
        }
    }


    public String replaceSqlKey(String str) {
        String retStr = str;
        for (int i = 0, size=keyValues.length; i < size; i++) {
            String [] words = keyValues[i];
            if (str.indexOf(words[0]) >= 0) {
                retStr = retStr.replaceAll(words[0], words[1]);
            }
        }
        return retStr;
    }

自测

我们为什么要自测,自测的目的是什么呢?在回答这个问题前,我们先来了解一下程序开发过程中,发生的一些问题,以及我们该如何去避免。
在编码结束后,测试部门进行测试的时候,发现程序的流程没法连贯起来,甚至直接报错,无法进行后面的测试工作。程序员自测可以减少这部分的问题,至少能保证代码是可以正常运行的,开发人员自已发现的问题,解决起来是最快的,没有跟其它人的沟通成本;而且开发人员在自测过程中发现的一些问题,解决后,可以自己总结,从而避免在以后的开发过程中发生类似的情况。
我们一般认为研发人员对自己开发的模块进行自测,是应该的,用研发术语来讲是默认的,不需要另行强调。程序猿的工作是团队协作中的一环,应该对自己所做的工作负责。
专业的测试人员也是不希望拿到漏洞百出的程序的。所以,最终,有些问题,我们是绕不开的,那就是提高程序猿的基本职业素养。

我在网上找了两个例子说明自测的重要性:
编程规范(自测的重要性)http://m.blog.csdn.net/article/details?id=7296972
程序猿自测的重要性(职业素养必备)http://blog.csdn.net/shuaihj/article/details/43272153

数据库设计

数据库这块有些人可能认为如果公司有dba了,那么我还去了解数据库设计干嘛?但是做后端的开发就是这么心累,因为你是做为后端业务开发,你需要考虑到数据结构是否满足业务需求,性能需求,以及是否能满足以后可预见的扩展,不然,一旦数据结构发生很大的变化,那么业务层的代码也会需要做很大的调整,所以做为后端开发,你还得有良好的沟通和协调能力,跟产品设计人员和dba进行讨论,得出一个较好的思路或方案。

  1. 数据库数据逻辑删除
    对于开发的程序系统来说,数据库就是存放数据的地方,如果我们从数据库中将数据使用delete语句将数据物理删除掉了,那么有可能那条数据就是永远找不回来了,想想,如果在写delete功能的时候,将其中的delete语句的where条件少加了一个发布到线上,这将是多可怕的事情。

    删除这个操作对于用户来说就是不可见,那么在设计删除功能的时候,可以加一个删除标记来标志数据对用户来说是否可见,比如字段:del_flag,这样我们就使用update语句去操作,这样数据的安全性也会提升很多,而且如果数据被错误“删除”或用户希望把数据找回来,对于系统依然还是可以查询得到。

  2. 主键(id)类型的字段风格统一
    在这里我要说说为什么是主键风格而不是字段类型,因为在主键里面,它涉及到数据类型,字段大小,以及id是否自增长等等。

    1. 主键id的风格统一有什么好处呢?首先对于其它他通过引用id关联别一张的数据,那么在设计的时候,就知道这个关联id只需要设置成主键风格的字段就好了。
    2. mybatis生成empty类,dao接口,mapper.xml配置文件的主键id字段和关联字段风格统一,在java代码中的对对应的方法和参数的访问都不需要特别去注间,也不需要特意的去转换。
  3. 表公共字段

    create_time, update_time, del_flag
    create_time 创建时间
    update_time 更新时间
    del_flag 删除标记(1:删除,0:否)
    这三个字段并不是说要求每张表都需要去创建,而是看业务是否需要,比如说,有删除需求或业务的数据,那么就可以创建del_flag字段。如果在分析中需要用到数据的是创建时间,或生成时间,那么可以使用create_time字段,这个字段应该也是大部分表都会创建的。另外就是update_time这个字段,同样是需求驱动,如果一条数据在以后会执行update操作,那么就可以创建update_time这个字段。

  4. 适当的使用冗余字段避免多表查询
    在mysql中,不太可能会去写几百行的sql查询,而且多表查询对于mysql来说,会消耗更多的资源,那么会导致sql查询慢,影响程序对请求的响应时间,影响程序的并发量,但是适当添加冗余字段和关联表,可以避免多表关联查询,从而解决前面的问题。

    • 举个冗余字段的例子

    地区表area

    字段名 描述
    id
    pid 上级id
    area_name 地区名称

    学校表school

    字段名 描述
    id
    area_id 地区Id
    school_name 学校名称

    如果在列表中需要显示school_name,area_name,那么就是需要关联查询了:

    select a.school_name, b.area_name form school a join area b on a.area_id=b.id

    但是其实只要在school表中添加一个area_name字段,就可以避免这种关联查询了呀。
    学校表school

    字段名 描述
    id
    area_id 地区Id
    school_name 学校名称
    area_name 地区名称(冗余字段)
    select school_name, area_name form school 
  5. sql where 条件注意事项

    1. 避免检索不需要的列
      在检索记录且需要显示的列较多时,尽量不要使用“*”来代替所有列,这样不仅增加处理时间(MySQL在解析的过程中,会将“*”依次转换成所有的列名,这个工作是通过查询数据字典完成的, 这意味着将耗费更多的时间,同时也增加了I/O的量),而且当表结构变化时,原来的列顺序有可能完全改变而导致不必要的bug或修改。
    2. 避免检索列为null
      避免在WHERE谓词中包含IS NULL、IS NOT NULL的筛选条件。因为索引的创建是对应具体值而存在,既然为NULL也就没有对应的值,索引自然就会失去索引功能,从而性能会大幅下降。
    3. 避免使用in,not in;使用exists代替in。
      在子查询中使用了NOT IN演算后,会发生内部排序、合并处理,为了提高性能,可以用NOT EXISTS来代替NOT IN + 子查询。但并非所有情况都适用。具体要依据实测结果而定。
    4. 避免使用子查询和exists,用表连接代替。
    5. 避免多表关联查询,必要的时候建冗余字段,建中间表。
    6. 用UNION ALL代替UNION
      • Union 对两个结果集进行并集操作,重复数据只显示一次,存在排序操作。
      • Union All对两个结果集进行并集操作,重复数据全部显示。不存在排序操作。
    7. 模糊查询

      模糊查询主要有以下三种形式:

      后模糊:col like ‘ABC%’
      前模糊:col like ‘%ABC’
      全模糊:col like ‘%ABC%’

      • 后模糊是最好的,在col字段上建立索引是可以被优化器选择的,并且是效率比较高的索引范围扫描方式,所以要尽量采取或转换成这种形式。
      • 前模糊形式即使在col字段上建立索引通常也还是不会被使用,即使使用效率也不会太高。所以这种方式是要尽量避免的,或者采取一些变通的手段比如采取反转函数等,但基本都需要改写原来的代码。
      • 全模糊形式普通索引的效率也会很差,写法上也是需要尽量避免的。虽然可以采用全文索引的方式来达到提高索引效率的目的,但全文索引相对复杂且占用空间要比普通索引大很多,且索引维护时要消耗更多资源,设计上需要综合考虑
    8. 避免类型转换
      隐式转换导致索引失效,这一点应当引起重视,也是开发中经常会犯的错误,由于表的字段t1定义为varchar(20),但在查询时把该字段作为number类型以where条件传给MySQL ,这样会导致索引失效。

      /*低效:*/
      SELECT * FROM test WHERE t1 =13333333333;
      
      /*高效:*/
      SELECT * FROM test WHERE t1 ='13333333333';
    9. 避免索引列参杂计算
      检索条件中索引列被参与计算,或被用作函数的参数,那么就会失去该列的索引功能,从而导致性能急剧下降。可以通过建函数索引的方法,计算结果或函数值事前计算好作为索引来用。

      /*低效:*/
      SELECT * FROM test WHERE sal*1.1 > 950;
      /*高效:*/
      SELECT * FROM test WHERE sal > 950/1.1;
      
      /*低效:*/
      SELECT * FROM test WHERE name || type = 'XXXY' ;
      /*高效:*/ 
      SELECT * FROM test WHERE name = 'XXX' AND type = 'Y' ;
      
      /*低效:*/
      SELECT * FROM test WHERE TO_CHAR(hiredate, ‘YYYYMMDD’) = '20100722';
      /*高效:*/
      SELECT * FROM test WHERE hiredate = TO_DATE('20100722' , 'YYYYMMDD') ;
      
      /*低效:*/
      SELECT * FROM test WHERE SUBSTR(name, 1, 7) = 'CAPTIAL';
      /*高效:*/
      SELECT * FROM test WHERE name LIKE 'CAPTIAL%';
      
      /*低效:*/
      SELECT * FROM test WHERE TRUNC(trans_date) = TRUNC(SYSDATE);
      /*高效:*/
      SELECT * FROM test WHERE trans_date BETWEEN TRUNC(SYSDATE) AND TURNC(SYSDATE) + .99999

      上面介始一些数据库设计和写sql时需要关注的地方,但是数据库设计并不是死板的,它是灵活的,在设计数据库表结构时,需要更多的去考虑是否符合现有业务需求,以及以后的一个扩展,这些都需要跟产品设计人员和dba进行反复的讨论,才能得出结论。

redis 的使用场景

  1. 缓存
    现在大部分程序员都知道redis可以用来作分布式缓存,但是了解redis么?redis有哪些数据类型?redis在做分布式缓存方面有哪些需要注意的呢?如果不知道的可以往下看,知道的,但是还是想了解一下,也可以往下看。

    • redis的数据类型

      1. string
        string类型非常明显,就是存放字符串的,当然也可以存放二进制内容。可以存数值,然后使用INCR,INCRBY,INCRBYFLOAT命令对数值进行增量操作, 如果把第二参数设置为负数,也可以进行减量操作。部分命令如下:

        命令 命令格式 描述
        APPEND APPEND key value 如果 key 已经存在并且是一个字符串, APPEND 命令将 value 追加到 key 原来的值的末尾。如果 key 不存在, APPEND 就简单地将给定 key 设为 value ,就像执行 SET key value 一样。
        GETSET GETSET key value 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
        DECR DECR key 将 key 中储存的数字值减一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECR 操作。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。
        DECRBY DECRBY key decrement 将 key 所储存的值减去减量 decrement 。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECRBY 操作。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。
        INCR INCR key 将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。
        INCRBY INCRBY key increment 将 key 所储存的值加上增量 increment 。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。
        INCRBYFLOAT INCRBYFLOAT key increment 为 key 中所储存的值加上浮点数增量 increment 。如果 key 不存在,那么 INCRBYFLOAT 会先将 key 的值设为 0 ,再执行加法操作。如果命令执行成功,那么 key 的值会被更新为(执行加法之后的)新值,并且新值会以字符串的形式返回给调用者。更多介绍请看http://doc.redisfans.com/string/incrbyfloat.html
        STRLEN STRLEN key 返回 key 所储存的字符串值的长度。
        SETEX SETEX key seconds value 将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。如果 key 已经存在, SETEX 命令将覆写旧值。
        SETNX SETNX key value 将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写

        更多的命令可以查看http://doc.redisfans.com/string/index.html

      2. list
        这个是列表类型,它提供了列表的功能,但是它也可以充当队列的解决,它提供了以下几个队列的功能:

        命令 描述
        LPOP 移除并返回列表 key 的头元素。
        RPOP 移除并返回列表 key 的尾元素。
        BLPOP 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。接受超时时间
        BRPOP 它是 RPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发现可弹出元素为止。接受超时时间
        RPOPLPUSH 该命令在一个原子时间内,执行以下两个动作:(1)将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。(2)将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。
        BRPOPLPUSH BRPOPLPUSH 是 RPOPLPUSH 的阻塞版本,当给定列表 source 不为空时, BRPOPLPUSH 的表现和 RPOPLPUSH 一样。当列表 source 为空时, BRPOPLPUSH 命令将阻塞连接,直到等待超时,或有另一个客户端对 source 执行 LPUSH 或 RPUSH 命令为止。接受超时时间

        更多的命令可以查看http://doc.redisfans.com/list/index.html

      3. set
        这是集合类型,重复的元素将不会被添加进去,看下面的例子

        
        # 添加单个元素
        
        
        redis> SADD bbs "discuz.net"
        (integer) 1
        
        
        
        # 添加重复元素
        
        
        redis> SADD bbs "discuz.net"
        (integer) 0
        
        
        
        # 添加多个元素
        
        
        redis> SADD bbs "tianya.cn" "groups.google.com"
        (integer) 2
        
        redis> SMEMBERS bbs
        1) "discuz.net"
        2) "groups.google.com"
        3) "tianya.cn"

        更多的命令可以查看http://doc.redisfans.com/set/index.html

      4. sortset
        这个类型跟set类似,但是它是可排序的,除了增删除改查外,比较常用的命令还有:

        命令 命令格式 描述
        ZINCRBY ZINCRBY key increment member 为有序集 key 的成员 member 的 score 值加上增量 increment 。可以通过传递一个负数值 increment ,让 score 减去相应的值,比如 ZINCRBY key -5 member ,就是让 member 的 score 值减去 5 。当 key 不存在,或 member 不是 key 的成员时, ZINCRBY key increment member 等同于 ZADD key increment member 。
        ZCOUNT ZCOUNT key min max 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。
        ZRANGE ZRANGE key start stop [WITHSCORES] 返回有序集 key 中,指定区间内的成员。其中成员的位置按 score 值递增(从小到大)来排序。具有相同 score 值的成员按字典序(lexicographical order )来排列。如果你需要成员按 score 值递减(从大到小)来排列,请使用 ZREVRANGE 命令。
        ZRANGEBYSCORE ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。具有相同 score 值的成员按字典序(lexicographical order)来排列(该属性是有序集提供的,不需要额外的计算)。
        ZRANK ZRANK key member 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。排名以 0 为底,也就是说, score 值最小的成员排名为 0 。使用 ZREVRANK 命令可以获得成员按 score 值递减(从大到小)排列的排名。
        ZREVRANK ZREVRANK key member 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递减(从大到小)排序。排名以 0 为底,也就是说, score 值最大的成员排名为 0 。

      还有更多的命令可以到查看http://doc.redisfans.com/sorted_set/index.html

      1. hash
        这是个哈希表,类似于Java中的HashMap,常用的命令如下:

        命令 命令格式 描述
        HSET HSET key field value 将哈希表 key 中的域 field 的值设为 value 。如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。如果域 field 已经存在于哈希表中,旧值将被覆盖。
        HSETNX HSETNX key field value 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在。若域 field 已经存在,该操作无效。如果 key 不存在,一个新哈希表被创建并执行 HSETNX 命令。
        HMSET HMSET key field value [field value …] 同时将多个 field-value (域-值)对设置到哈希表 key 中。此命令会覆盖哈希表中已存在的域。如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。
        HMGET HMGET key field [field …] 返回哈希表 key 中,一个或多个给定域的值。如果给定的域不存在于哈希表,那么返回一个 nil 值。因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。
        HLEN HLEN key 返回哈希表 key 中域的数量。
        HKEYS HKEYS key 返回哈希表 key 中的所有域。
        HVALS HVALS key 返回哈希表 key 中所有域的值。
        HGET HGET key field 返回哈希表 key 中给定域 field 的值。
        HGETALL HGETALL key 返回哈希表 key 中,所有的域和值。在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。
        HEXISTS HEXISTS key field 查看哈希表 key 中,给定域 field 是否存在。
        HDEL HDEL key field [field …] 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
        HINCRBY HINCRBY key field increment 为哈希表 key 中的域 field 的值加上增量 increment 。增量也可以为负数,相当于对给定域进行减法操作。如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。如果域 field 不存在,那么在执行命令前,域的值被初始化为 0 。对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。本操作的值被限制在 64 位(bit)有符号数字表示之内。
        HINCRBYFLOAT HINCRBYFLOAT key field increment 为哈希表 key 中的域 field 加上浮点数增量 increment 。如果哈希表中没有域 field ,那么 HINCRBYFLOAT 会先将域 field 的值设为 0 ,然后再执行加法操作。如果键 key 不存在,那么 HINCRBYFLOAT 会先创建一个哈希表,再创建域 field ,最后再执行加法操作。当以下任意一个条件发生时,返回一个错误:(1)域 field 的值不是字符串类型(因为 redis 中的数字和浮点数都以字符串的形式保存,所以它们都属于字符串类型)。(2)域 field 当前的值或给定的增量 increment 不能解释(parse)为双精度浮点数(double precision floating point number)。
  2. Redis 不支持回滚(roll back)
    如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。
    以下是这种做法的优点:

    • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
    • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

    有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。
    鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。

    使用事务时可能会遇上以下两种错误:

    1. 事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
    2. 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。
      对于发生在 EXEC 执行之前的错误,客户端以前的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED ,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。

    不过,从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。
    在 Redis 2.6.5 以前, Redis 只执行事务中那些入队成功的命令,而忽略那些入队失败的命令。 而新的处理方式则使得在流水线(pipeline)中包含事务变得简单,因为发送事务和读取事务的回复都只需要和服务器进行一次通讯。
    至于那些在 EXEC 命令执行之后所产生的错误, 并没有对它们进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。
    从协议的角度来看这个问题,会更容易理解一些。 以下例子中, LPOP 命令的执行将出错, 尽管调用它的语法是正确的:

    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    
    MULTI
    +OK
    
    SET a 3
    abc
    
    +QUEUED
    LPOP a
    
    +QUEUED
    EXEC
    
    *2
    +OK
    -ERR Operation against a key holding the wrong kind of value

    EXEC 返回两条批量回复(bulk reply): 第一条是 OK ,而第二条是 -ERR 。 至于怎样用合适的方法来表示事务中的错误, 则是由客户端自己决定的。
    最重要的是记住这样一条, 即使事务中有某条/某些命令执行失败了, 事务队列中的其他命令仍然会继续执行 —— Redis 不会停止执行事务中的命令。

  3. redis做为分布式缓存的注意事项
    作为缓存服务器,如果不加以限制内存的话,就很有可能出现将整台服务器内存都耗光的情况,可以在redis的配置文件里面设置:

    
    # 限定最多使用1.5GB内存
    
    maxmemory 1536mb

    如果内存到达了指定的上限,还要往redis里面添加更多的缓存内容,需要设置清理内容的策略:

    
    # 设置策略为清理最少使用的key对应的数据
    
    maxmemory-policy allkeys-lru

    清理策略有多种,redis的官方文档有一篇很详细的说明: http://redis.io/topics/lru-cache
    redis提供了INFO这个命令,能够随时监控服务器的状态,只用telnet到对应服务器的端口,执行命令即可:

    telnet localhost 6379
    info

    在输出的信息里面有这几项和缓存的状态比较有关系:

    keyspace_hits:14414110
    keyspace_misses:3228654
    used_memory:433264648
    expired_keys:1333536
    evicted_keys:1547380

    通过计算hits和miss,我们可以得到缓存的命中率:14414110 / (14414110 + 3228654) = 81% ,一个缓存失效机制,和过期时间设计良好的系统,命中率可以做到95%以上,对于整体性能提升是很大的。
    used_memory,expired_keys,evicted_keys这3个信息的具体含义,redis的官方也有一篇很详细的说明: http://redis.io/commands/info
    有个ruby gem叫redis-stat,它利用INFO命令展现出更直观的信息报表,推荐:
    https://github.com/junegunn/redis-stat

    redis 缓存的单个value不能太大,因为redis的工作线程是单线程模式,redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。
    不过,因为一般的内存操作都是简单存取操作,线程占用时间相对较短,主要问题在io上,因此,redis这种模型是合适的,但是如果某一个线程出现问题导致线程占用很长时间,那么reids的单线程模型效率可想而知.
    不过,因为一般的内存操作都是简单存取操作,线程占用时间相对较短,主要问题在io上,因此,redis这种模型是合适的,但是如果某一个线程出现问题导致线程占用很长时间,那么reids的单线程模型效率可想而知.

    引自网络:
    总体来说快速的原因如下: 
1)绝大部分请求是纯粹的内存操作(非常快速) 
2)采用单线程,避免了不必要的上下文切换和竞争条件 
3)非阻塞IO 
内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间 

这3个条件不是相互独立的,特别是第一条,如果请求都是耗时的,采用单线程吞吐量及性能可想而知了。应该说redis为特殊的场景选择了合适的技术方案。

    而且一个字符串类型的value最式可以容纳的数据长度是512M,但是如果存一个1M的value进去,当读取到这个value时,在1000M光纤的网络环境中,不算其它消耗,光算网络消耗,redis的吞吐量就下降了1/1000,这在高并发的情况下,异常可怕,如果有1000个请求都读到这个value,那么redis的吞吐量就只有1000了。

    1. 所以redis中存储的每一个value的字节大小不能大于5kb,如果大于5kb,就另外存一个key.
    2. 在存入每个缓存时,都要考虑一下这个缓存的时效,给出相应的过期时间.(请查看EXPIREAT和EXPIRE命令)

    参考资料:
    http://www.open-open.com/lib/view/open1419670554109.html
    https://my.oschina.net/zhenglingfei/blog/409925
    http://www.tuicool.com/articles/UjyEZn

  4. redis缓存例子-session共享
    新建一个SessionManager类,继承org.apache.catalina.session.ManagerBase类,实现org.apache.catalina.Lifecycle接口,在其中重写以下方法:

    //org.apache.catalina.session.ManagerBase
    Session createSession(String sessionId) 
    Session findSession(String id)
    Session createEmptySession() 
    void remove(Session session) 
    
    //org.apache.catalina.Lifecycle
    addLifecycleListener(LifecycleListener listener) 
    LifecycleListener[] findLifecycleListeners() 
    void removeLifecycleListener(LifecycleListener listener) 

    在tomcat的context.xml中加入

    <Manager className="**.****.**SessionManager"/>

    这样就好了。
    可以参考redisson-tomcat,目前支持tomcat6,tomcat7,tomcat8 。 github项目路径在https://github.com/redisson/redisson/tree/master/redisson-tomcat

  5. 全局sequence生成器
    使用string类型的INCR命令就可以实现了。

    redis> SET page_view 20
    OK
    
    redis> INCR page_view
    (integer) 21
    
    redis> GET page_view    # 数字值在 Redis 中以字符串的形式保存
    "21"
  6. 队列
    使用list(列表)类型的LPOP,RPOP,RPOPLPUSH,BLPOP,BRPOP,BRPOPLPUSH就可以实现。

    redis> LLEN course
    (integer) 0
    
    redis> RPUSH course algorithm001
    (integer) 1
    
    redis> RPUSH course c++101
    (integer) 2
    
    redis> LPOP course  # 移除头元素
    "algorithm001"
  7. 监听器(发布订阅)
    有兴趣的同学可以查看http://doc.redisfans.com/topic/pubsub.html

  8. 分布式锁
    redisson是一个用于连接redis的java客户端工作,相对于jedis,是一个采用异步模型,大量使用netty promise编程的客户端框架。
    redisson通过redis实现了不少功能, 分布式锁就是其中一种,在其中有一个RLock接口,而其实现类是RedissonLock, 下面来大概看看实现原理. 先看看 (3) 中例子执行时, 所运行的命令(通过monitor命令):

    127.0.0.1:6379> monitor
    OK
        1434959509.494805 [0 127.0.0.1:57911] "SETNX" "haogrgr" "{\"@class\":\"org.redisson.RedissonLock$LockValue\",\"counter\":1,\"id\":\"c374addc-523f-4943-b6e0-c26f7ab061e3\",\"threadId\":1}"
        1434959509.494805 [0 127.0.0.1:57911] "GET" "haogrgr"
        1434959509.524805 [0 127.0.0.1:57911] "MULTI"
        1434959509.529805 [0 127.0.0.1:57911] "DEL" "haogrgr"
        1434959509.529805 [0 127.0.0.1:57911] "PUBLISH" "redisson__lock__channel__{haogrgr}" "0"
        1434959509.529805 [0 127.0.0.1:57911] "EXEC"
    可以看到, 大概原理是, 通过判断Redis中是否有某一key, 来判断是加锁还是等待, 最后的publish是一个解锁后, 通知阻塞在lock的线程.
    分布式锁的实现依赖的单点, 这里Redis就是单点, 通过在Redis中维护状态信息来实现全局的锁. RedissonLock还实现了可重入, 保证原子性等等细节.
    

    想了解Redisson的同学可以查看它的git项目https://github.com/redisson/redisson, https://github.com/hkleecn/redisson 这两网站我目前也不知道哪个是正版。
    另外参考了:
    http://www.tuicool.com/articles/BjyeaeQ
    http://blog.csdn.net/lzlhen1988/article/details/47832237
    http://yunshen0909.iteye.com/blog/2291217

功能设计需要避免的一些经验分享

  1. C 端应用在读到列数据时,需分页,不能让单个请求占用数据库连接,web 容器线程的时间太久,不然高并发下的吞吐量就会受到影响。
  2. 对于一些付费的第三方接口,需要按业务做严格的校验。

    1. 比如发送验证码短信

      • 一个手机号要有指定的一个较短时间内(一分钟)不能再次发送
      • 一个小机号指定时间段内(1小时)不能发送指定条数。
      • 一个ip一段时间内发送的短信不能超过指定数量

      类似于这样的校验需要有。

    2. 身份证验证也是一样要有时间上的校验,不然一旦被人攻击,将会付出大额的金钱。

上面这两个例子是因为我们会接触到,所以当典型例子来列举,当然还会有其它的场景,大家在涉及这种类型的业务时,需要注意多思考一下安全、防刷方面的校验。

交流题

最后给两个交流题吧,看看哪些猿的脑阔里面装的贷比较多。
1. 一个百万级别的数据库表,如果要尽快的从数据库中导出到一个文件中,你会怎么样去设计?
2. 一段五十万个字的字符串,要怎么设计才能尽量快的从中随机查找一个段相连的字符串?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章