用redis操做缓存来实现分布式锁实例

目前几乎全部的大型网站及应用都是采用分布式部署的方式,分布式系统开发带来的优势不少,高可用,高并发,水平扩展,分开部署等。但分布式的开发也带来了一些新问题,有的时候,咱们须要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了不少并发处理相关的API  ,也就是咱们常说的“锁”(如synchronized,lock),可是这些API在分布式场景中就无能为力了,也就是说Java没有提供分布式锁的功能。html

基于分布式锁的实现有多种方案,常见的基于数据库自己的锁来实现,或者基于zookeeper的API实现,或者是基于缓存来实现分布式锁等等,这些方案都各有可取之处,今天咱们介绍的是基于redis的缓存实现分布式锁的方案,你们若是对其余方案有兴趣的能够上网搜索研究。git

redis是基于key-value的一种NoSql数据库,普遍应用于分布式的应用中,通常用于放置缓存数据。安装的方法也比较简单,楼主安装的是windows版本的,选择最新的zip版,下载完以后直接解压便可。下载地址:https://github.com/MicrosoftArchive/redis/tagsgithub

redis中有一个命令setnx (SET IF NOT EXISTS) , 若是不存在,就设置key,将 key 的值设为 value,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不作任何动做。基于这个特性,咱们能够对须要锁住的对象加上key,这样,同一时间就只能有一个线程拥有这把锁,从而达到分布式锁的效果。下面用一个具体的Java实例来展现redis的分布式锁效果。redis

Java操做redis须要用到第三方的库类,因此先在pom.xml中引入依赖。数据库



加入依赖后,作一个redis的工具方法,分别实现的是加锁和解锁的功能。windows

public class RedisLock {

    @Autowired  private StringRedisTemplate redisTemplate;   /**  * 加锁  *  * @param key  * @param value 当前时间+超时时间  * @return  */  public boolean lock(String key, String value) {
        //至关于setnx命令  if (redisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;  }
        //下面的这段代码是判断以前加的锁是否超时,是的话就更新,必定要加这段代码  //否则就有可能出现死锁。  String currentValue = redisTemplate.opsForValue().get(key);  //若是锁过时  if (!StringUtils.isEmpty(currentValue)
                && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            //获取上一个锁的时间,这段代码的判断是防止多线程进入这里,只会有一个线程拿到锁  String oldValue = redisTemplate.opsForValue().getAndSet(key, value);  if (!StringUtils.isEmpty(oldValue)
                    && oldValue.equals(currentValue)) {
                return true;  }
        }
        return false;  }

    /**  * 解锁  *  * @param key  * @param value  */  public void unLock(String key, String value) {
        try {
            String currentValue = redisTemplate.opsForValue().get(key);  if (!StringUtils.isEmpty(currentValue)
                    && currentValue.equals(value)) {
                redisTemplate.opsForValue().getOperations().delete(key);  }
        } catch (Exception e) {
            log.error("【redis分布式锁】 解锁异常,{}", e);  }
    }
}

如今,咱们模拟一个下单的场景,假设有一个秒杀的活动,同一时间有多个线程对同一个产品进行访问,而后分别看看加锁和没加锁的结果来作对比。下面是秒杀的模拟代码:浏览器

public class SecKillController {

    @Autowired  private SecKillService secKillService;   /**  * 查询秒杀活动特价商品的信息  * @param productId  * @return  * @throws Exception  */  @GetMapping("/query/{productId}")
    public String query(@PathVariable String productId) throws Exception{
        return secKillService.querySecKillProductInfo(productId);  }

    /**  * 秒杀的方法  * @param productId  * @return  * @throws Exception  */  @GetMapping("/order/{productId}")
    public String skill(@PathVariable String productId) throws Exception{
        log.info("@skill request ,productId:" +productId);  secKillService.orderProductKill(productId);  return secKillService.querySecKillProductInfo(productId);  }
}

public class SecKillServiceImpl implements SecKillService {

    private static final int TIME_OUT = 1 * 1000;   @Autowired  private RedisLock redisLock;   static Map<String, Integer> products;  static Map<String, Integer> stock;  static Map<String, String> orders;   static {
        /**  * 模拟多个表,商品信息表,库存表,秒杀成功订单表  */  products = new HashMap<>();  stock = new HashMap<>();  orders = new HashMap<>();  products.put("123", 100000);  stock.put("123", 100000);  }

    /**  * @param productId 订单id  * @return  */  private String queryMap(String productId) {
        return "限量份数" + products.get(productId)
                + "还剩:" + stock.get(productId) + "份"  + "该商品成功下单用户数目:"  + orders.size() + "人";  }

    @Override  public String querySecKillProductInfo(String productId) {
        return this.queryMap(productId);  }

    @Override  public void orderProductKill(String productId) {

        //1.查询该商品库存,为0则活动结束  int stockNum = stock.get(productId);  if (stockNum == 0) {
            throw new RuntimeException("活动结束");  } else {
            //2.下单(模拟不一样用户openid不一样)  orders.put(KeyUtil.getUniqueKey(), productId);  //3.减库存  stockNum = stockNum - 1;  try {
                Thread.sleep(100);  } catch (InterruptedException e) {
                e.printStackTrace();  }
            stock.put(productId, stockNum);  }
    }
}

先模拟没加锁的下单状态,咱们开启工程后,用Apache ab做为压测工具来模拟高并发访问过程缓存


在浏览器上访问查询后的订单数量,结果显示以下:多线程


能够看到,再高并发的访问环境下,若是咱们没有对订单作锁的处理,那么就可能出现数据的紊乱,致使结果不对应,这显然不符合咱们的需求,下面咱们来看看加上redis锁以后的访问状况,先把service中的秒杀代码加上锁。并发

@Override public void orderProductKill(String productId) {

    //加锁,保证下面的代码单线程的访问  long time = System.currentTimeMillis() + TIME_OUT;  if (!redisLock.lock(productId, String.valueOf(time))) {
        throw new RuntimeException( "下单失败");  }

    //1.查询该商品库存,为0则活动结束  int stockNum = stock.get(productId);  if (stockNum == 0) {
        throw new RuntimeException("活动结束");  } else {
        //2.下单(模拟不一样用户openid不一样)  orders.put(KeyUtil.getUniqueKey(), productId);  //3.减库存  stockNum = stockNum - 1;  try {
            Thread.sleep(100);  } catch (InterruptedException e) {
            e.printStackTrace();  }
        stock.put(productId, stockNum);  }
    //解锁  redisLock.unLock(productId, String.valueOf(time)); }

 而后再进行一样的操做


咱们能够看到,加上锁以后的订单处理数量是正确的,也就是redis锁是起到了做用的,这是符合咱们的需求的。

上面的例子相对比较简单,由于精力能力有限,楼主无法给你们展现真正的分布式锁的实现效果,但从原理上实际上是同样的,都是用redis的setnx命令来加上锁,保证分布式环境下锁住的对象只能被一个线程访问,并且从实现方式上来讲也比较简单  (只须要一个命令就行,很深刻人心得意 ) ,所以,redis在分布式锁的应用中也被普遍使用。


本文分享 CSDN - 鄙人薛某。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索