一、加锁原因
  
  二、原子操作
  
  三、分布式锁
  
  四、分布式锁常见问题
  
  一、加锁原因
  
  在一些比较高并发的业务场景,经常听到通过加锁的方法实现线程安全。
  
  下面简单介绍一下
  
  1.1 加锁方式
  
  数据库锁
  
  数据库本身提供了锁机制,比如乐观锁、悲观锁等等。下面给出我之前写的一篇博客,介绍一下mysql数据库的锁机制
  
  Mysql的锁机制
  
  单体环境
  
  Java线程层面,Java的jdk本身就提供了,比如synchronized和ReentrantLock可重入锁。这是实现单体环境锁的一种方法,这里简单介绍一下,并不对synchronized和ReentrantLock进行详细介绍。
  
  分布式环境
  
  上面介绍的都是单体环境的和数据库层面的,下面介绍一下分布式环境的解决方法。分布式环境有两种比较常用的解决方法,一种是通过Zookeeper实现分布式;一种是通过Redis实现分布式锁。本博客比较详细地介绍一下redis分布式锁,对于Zookeeper分布式锁有时间再写博客介绍。
  
  1.2 业务场景
  
  为什么加锁?从业务来说其实就是有业务场景,技术层面是为了保证线程安全性,也就是说保证线程操作是原子操作。
  
  下面简单介绍一下一些业务场景
  
  比如电商的秒杀场景,这就是一个高并发场景了,假如一个用户购买了库存只有10的8件商品,另外一个用户也要购买5件商品,这两个用户是同时进行的,第一个用户买了8件,库存就只有2件了,第二个用户再买5件,注意是和第一个用户操作同时进行的,这时也是10-5,库存只有5件了。假如第一个用户抢占了,库存优先减8了,第一个用户进行操作,同时进行,获取到的库存是10,这时就会出现业务问题了。
  
  二、原子操作
  
  原子操作定义
  
  博客介绍一下原子操作,为什么说到原子操作呢?貌似和分布式锁不搭边,其实不是的,我们说加锁,其本质目的就是为了实现线程操作是原子性的,也就是原子操作。
  
  原子操作:是指不会被线程调度机制打断的操作,而且期间不会有任何上下文切换(context switch)。
  
  2.1 context switch
  
  上面介绍一下上下文切换(context switch),上下文切换是计算机的cpu从一个任务,或者说进程,从一个任务(进程)切换到另外一个任务(进程),期间确保任务(进程)不冲突的过程。
  
  在国外的whatis.techtarget网站有进行了比较详细的定义
  
  上下文切换定义
  
  三、分布式锁
  
  3.1 实现方式
  
  可以实现方式 setnx+expire
  
  在Redis中实现分布式锁,可以通过setnx和expire实现,setnx命令意思是set key if not exist,就是说已经有一个线程占用了,就不执行set key操作
  
  语法:setnx key value;expire是设置key的时间
  
  这里setnx key为tkey,value为tvalue
  
  >setnx tkey tvalue
  
  OK
  
  >get tkey
  
  tvalue
  
  >expire tkey 5
  
  OK
  
  >del tkey
  
  (integer) 1
  
  先setnx,然后再给key加一个时间5秒,5秒后自动释放锁。当然一个进程执行过程还没5秒也可以就直接删除key。那么假如在setnx过程出现异常,锁就不能释放。
  
  为了避免上面所说的分布式锁不能释放问题,开源社区有很多分布式解决方案,很多第三方库,直到redis2.8版本,作者给出了一个很好的解决方案。
  
  redis2.8版本对setnx命令和expire命令进行了拓展,使这两个命令可以同时执行,也可以理解为同个事务了。
  
  语法:
  
  setnx key ex time nx
  
  例子,设置tkey,时间为5秒
  
  setnx tkey ex 5 nx
  
  四、分布式锁常见问题
  
  4.1 超时问题
  
  假如在释放锁和另一个线程重新占用锁之间,执行时间过长,超过了锁的超时设置,这时候就会出现,第一个线程的锁已经被标记为过期了,可是在临界区的执行程序还没执行,也就是说锁并没有真正释放。这时候如果第二个线程持有了锁,就会出现临界区的代码不能正常串行执行,因为第一个线程的锁在临界区还没真正释放。
  
  这是一种比较常见的Redis锁不能释放的超时问题。
  
  通过网上资料,有提供了一种解决方案思路,是通过在set key的时候给value值加一个时间戳字符串或者一个特定的随机数,比如uuid,可以表示特定线程的标识,这个标识要唯一。
  
  然后在删除key,重新加锁的时候,校验这个value是否为第一个线程的,匹配正确才删除key,这是一种方案,当然并不是很好的解决方案,只能说是相对安全的,因为在高并发情况下面,线程的调用机制还是可以支持另外的线程持有锁的。
  
  4.2 集群环境
  
  下面介绍一下集群环境的锁问题,业务场景,假如一个线程在主服务器(master)释放锁的时候,master突然冗机了,也是就是说锁还没被释放。这时集群环境检测到master机器冗机,就切换从服务器(slave)作为主服务器,这时候,另外一个线程进来了,占有了同一个锁,也就是出现了两个线程同时占有同一个锁的情况了。通过keepalive和redis实现主从服务器自动failover的方式或许可以解决问题。因为并没有实践过,所以不做详细解释。这篇博客
  
  Redis自从自动failover或许可以参考。
  
  ReadLock算法
  
  集群环境的锁同步是一个难题。上面的仅仅是我的想法并没有实践过,最近找到一个算法可以解决,ReadLock算法。readlock-py库已经有对改算法进行实践。ReadLock算法简单原理就是通过先检测set是否成功,set成功之后才向所有节点发送指令,释放锁。本博客并不对ReadLock算法做详细介绍,有机会再写博客介绍。
  
  <dependency>
  
  <groupId>org.apache.tomcat.embed</groupId>
  
  <artifactId>tomcat-embed-jasper<www.thd540.com/ /artifactId>
  
  <scope>provided<www.dasheng178.com /scope>
  
  </dependency>
  
  <dependency>
  
  <groupId>javax.servlet</groupId>
  
  <artifactId>jstl</artifactId>
  
  </dependency>
  
  第四步:在SpringBoot的属性文件application.properties中配置JSP的路由
  
  spring.mvc.view.prefix=/
  
  spring.mvc.view.suffix=.jsp
  
  第五步:修改Maven的pom.xml文件打包方式改成war(默认打包Jar,打包Jar包的方式使用Idea启动是没什么问题,如果单独运行Jar包就找不到JSP文件,如果改成War包即可)
  
  <packaging>war</packaging>
  
  SpringBoot中使用Thymeleaf
  
  SpringBoot官方是推荐使用thymeleaf作为优选的视图解析器,所以SpringBoot对Thymeleaf的支持非常好,这里仅仅演示SpringBoot如何选用Thymeleaf作用默认视图解析器。
  
  第一步:导入Thymeleaf的依赖
  
  <dependency>
  
  <groupId>org.springframework.boot<www.quwanyule157.com /groupId>
  
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
  
  </dependency>
  
  第二步:创建存放Thymeleaf模板文件夹,在Resources目录下创建templates目录
  
  这个文件夹的名字可不是我么随便命名的啊,是SpringBoot在自动装配Thymeleaf视图解析器的时候就已经预定义好了,我们看一下它的定义源码。
  
  @ConfigurationProperties(prefix www.furggw.com= "spring.thymeleaf")
  
  public class ThymeleafProperties {
  
  private static final www.dasheng178.com Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
  
  public static final String DEFAULT_PREFIX = www.mcyllpt.com/"classpath:/templates/";
  
  public static final String DEFAULT_SUFFIX = ".html";
  
  }
  
  SpringBoot中使用Freemark
  
  第一步:导入Maven依赖
  
  <dependency>
  
  <groupId>org.springframework.boot</groupId>
  
  <artifactId>spring-boot-starter-freemarker</artifactId>
  
  </dependency>
  
  第二步:创建存放Freemark模板文件夹,在Resources目录下创建templates目录
  
  @ConfigurationProperties(prefix www.feifanyule.cn/= "spring.freemarker")
  
  public class FreeMarkerProperties extends AbstractTemplateViewResolverProperties {
  
  public static final String DEFAULT_TEMPLATE_LOADER_PATH = "classpath:/templates/";
  
  public static final String DEFAULT_www.wanchuang178.cn  PREFIX = "";
  
  public static final String DEFAULT_SUFFIX = ".ftl";
  
  }
  
  我们可以看到SpringBoot在自动装配Freemarker视图解析器默认是将模板文件放在classpath:/templates/路径内,我们同样可以在SpringBoot的配置文件中自行配置。
  
  小提示:我在写Freemark视图解析器的时候并没有将第一个JSP内部资源解析器给删除掉,所以他们是并存的,所以我们可以知道SpringBoot在装配他们的时候给予设定了优先级顺序。从下图可以看到他们的优先级顺序;Freemarker>Thymeleaf>InternalResourceViewResolver`

12-09 19:56