美文网首页设计方案我的微服务
7,Spring Boot整合Redis实现简单的分布式锁

7,Spring Boot整合Redis实现简单的分布式锁

作者: 滔滔逐浪 | 来源:发表于2020-02-20 16:01 被阅读0次

    1,为什么需要分布式锁?
    在开发应用时,当多个客户或者多个线程需要对某个共享的数据进行操作时,就需要使用线程同步。在Java开发中,对于单机应用,因为是在同一个JVM内部,所以我们可以采用Java提供的各种多线程操作的技巧来实现线程同步。

    而对于分布式系统来说,由于多个请求可能被分发到不同的机器上去处理,如果这多个请求都是对同一个资源进行操作,那么使用基本的Java多线程线程同步技术可能就解决不了这个问题。


    image

    如上图,请求A、B、C都是发起扣减同一个商品的库存操作,三个请求被分发到三台不同的服务部署机器上进行处理。而三台机器并不在同一个JVM,所以Java提供的线程同步技巧就发挥不了作用了。但是对于扣减库存这样的场景,必须要使用线程同步来保证同一个商品的库存不会被漏扣或者多扣。

    为了保证在高并发的场景下,临界资源(共享资源)同时只能被一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。在单机环境中,Java中提供了很多并发处理相关的API。

    但是在分布式系统中,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
    2、分布式锁应该具备哪些条件?

    在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
    高可用的获取锁与释放锁;
    高性能的获取锁与释放锁;
    具备可重入特性;
    具备锁失效机制,防止死锁;
    具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
    3、Spring Boot整合Redis实现简单的分布式锁
    实现思想

    SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
    expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
    delete key:删除key
    Spring Boot整合Redis实现简单的分布式锁实现细节
    我们以实现一个多个线程同步扣除同一个商品的库存为例,实现一个简单的Redis分布式锁。实例需要依赖的内容如下:

    Spring Boot Web 依赖:通过在页面上点击实现多个请求;
    Spring JPA:数据库访问;
    MySQL:存储商品库存;

    DROP TABLE IF EXISTS `goods_store`;
    CREATE TABLE `goods_store` (
      `code` varchar(255) NOT NULL,
      `store` int(255) DEFAULT NULL,
      `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`code`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    -- ----------------------------
    -- Records of goods_store
    -- ----------------------------
    INSERT INTO `goods_store` VALUES ('2019053016502800101', '1968', '2020-02-20 15:54:02');
    
    
    
    
    
    

    html:页面模板;
    pom.xml:

    <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
    
    

    依赖配置好之后,如果没有安装Redis和MySQL的,自己先安装好Redis和MySQL,
    准备工作做好之后,在Spring Boot配置文件中配置Redis、MySQL的链接属性、Spring Boot应用端口、名称等。
    yml配置:

    spring:
      application:
        name: Redis Distribute Lock
    
      redis:
        host: 127.0.0.1
        port: 6379
        timeout: 20000
        password: 654321
        jedis:
          pool:
            max-active: 8
            min-idle: 0
            max-idle: 8
            max-wait: -1
          
      datasource:
        url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull&useSSL=true&allowMultiQueries=true&serverTimezone=Asia/Hong_Kong
        username: root
        password: 111111
        driver-class-name: com.mysql.jdbc.Driver
          
      jpa:
        show-sql: true
        hibernate:
          ddl-auto: none
          
    server:
      port: 8090
    
    
    

    定义实体:

    package com.taotao.redisson.entity;
    
    import lombok.Data;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.Id;
    import javax.persistence.Table;
    import java.io.Serializable;
    import java.util.Date;
    
    @Data
    @Entity
    @Table(name="goods_store")
    public class GoodsStore implements Serializable{
        private  static  final long serialVersionUID=1L;
      @Id
        private  String code;
    
        @Column(name="store")
        private  int store;
         @Column(name="update_time")
       private Date update_time;
    
    
    
    
    }
    
    
    

    实现Redis分布式锁

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    @Component
    public class RedisLock {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        
        /**
         * 加锁
         * @param lockKey 加锁的Key
         * @param timeStamp 时间戳:当前时间+超时时间
         * @return
         */
        public boolean lock(String lockKey,String timeStamp){
            if(stringRedisTemplate.opsForValue().setIfAbsent(lockKey, timeStamp)){
                // 对应setnx命令,可以成功设置,也就是key不存在,获得锁成功
                return true;
            }
            
            //设置失败,获得锁失败
            // 判断锁超时 - 防止原来的操作异常,没有运行解锁操作 ,防止死锁
            String currentLock = stringRedisTemplate.opsForValue().get(lockKey);
            // 如果锁过期 currentLock不为空且小于当前时间
            if(!StringUtils.isEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()){
                //如果lockKey对应的锁已经存在,获取上一次设置的时间戳之后并重置lockKey对应的锁的时间戳
                String preLock = stringRedisTemplate.opsForValue().getAndSet(lockKey, timeStamp);
                
                //假设两个线程同时进来这里,因为key被占用了,而且锁过期了。
                //获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
                //而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的timeStamp已经变成了B。
                //只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
                if(!StringUtils.isEmpty(preLock) && preLock.equals(currentLock)){
                    return true;
                }
            }
            
            return false;
        }
        
        /**
         * 释放锁
         * @param lockKey
         * @param timeStamp
         */
        public void release(String lockKey,String timeStamp){
            try {
                String currentValue = stringRedisTemplate.opsForValue().get(lockKey);
                if(!StringUtils.isEmpty(currentValue) && currentValue.equals(timeStamp) ){
                    // 删除锁状态
                    stringRedisTemplate.opsForValue().getOperations().delete(lockKey);
                }
            } catch (Exception e) {
                System.out.println("警报!警报!警报!解锁异常");
            }
        }
    }
    
    
    

    创建库存Respository

    package com.taotao.redisson.respository;
    
    import com.taotao.redisson.entity.GoodsStore;
    import io.lettuce.core.dynamic.annotation.Param;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.Modifying;
    import org.springframework.data.jpa.repository.Query;
    
    import javax.transaction.Transactional;
    
    /**
     * 库存Respository
     *
     */
    public interface GoodsStoreResponsitory  extends JpaRepository<GoodsStore,String>{
    
        @Modifying
        @Transactional
        @Query("update GoodsStore  gs set gs.store=gs.store-?2 where gs.code=?1")
        int updateStore(@Param("code")String code,@Param("store")Integer store);
    }
    
    

    创建库存接口,定义更新库存和获取库存信息的方法。

    package com.taotao.redisson.facade;
    
    import com.taotao.redisson.entity.GoodsStore;
    
    /**
     * 创建库存的接口,定义更新库存和获取库存信息的方法
     */
    public interface GoodsStoreFacade {
      /**
         * 根据产品编号更新库存
         * @param code
         * @param count
         * @return
         */
        String updateGoodsStore(String code,int count);
    
        /**
         * 获取库存的对象
         * @param code
         * @return
         */
        GoodsStore getGoodsStore(String code);
    }
    
    
    
    

    实现更新库存接口

    package com.taotao.redisson.service;
    
    import com.taotao.redisson.entity.GoodsStore;
    import com.taotao.redisson.facade.GoodsStoreFacade;
    import com.taotao.redisson.lock.RedisLock;
    import com.taotao.redisson.respository.GoodsStoreResponsitory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.Optional;
    
    
    
    /**
     * 库存管理服务
     * @author user
     *
     */
    @Service
    public class GoodsStoreService implements GoodsStoreFacade{
        @Autowired
        private GoodsStoreRespository goodsStoreRespository;
        
        @Autowired
        private RedisLock redisLock;
        
        /**
         * 超时时间 5s
         */
        private static final int TIMEOUT = 5*1000;
        
        /**
         * 根据产品编号更新库存
         * @param code
         * @return
         */
        @Override
        public String updateGoodsStore(String code,int count) {
            //上锁
            long time = System.currentTimeMillis() + TIMEOUT;
            if(!redisLock.lock(code, String.valueOf(time))){
                return "排队人数太多,请稍后再试.";
            }
            System.out.println("获得锁的时间戳:"+String.valueOf(time));
            try {
                GoodsStore goodsStore = getGoodsStore(code);
                if(goodsStore != null){
                    if(goodsStore.getStore() <= 0){
                        return "对不起,卖完了,库存为:"+goodsStore.getStore();
                    }
                    if(goodsStore.getStore() < count){
                        return "对不起,库存不足,库存为:"+goodsStore.getStore()+" 您的购买数量为:"+count;
                    }
                    System.out.println("剩余库存:"+goodsStore.getStore());
                    System.out.println("扣除库存:"+count);
                    goodsStoreRespository.updateStore(code, count);
                    try{
                        //为了更好的测试多线程同时进行库存扣减,在进行数据更新之后先等1秒,让多个线程同时竞争资源
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    return "恭喜您,购买成功!";
                }else{
                    return "获取库存失败。";
                }
            } finally {
                //释放锁
                redisLock.release(code, String.valueOf(time));
                System.out.println("释放锁的时间戳:"+String.valueOf(time));
            }
        }
        
        /**
         * 获取库存对象
         * @param code
         * @return
         */
        @Override
        public GoodsStore getGoodsStore(String code){
            Optional<GoodsStore> optional = goodsStoreRespository.findById(code);
            return optional.get();
        }
    }
    
    
    
    
    
    
    
    
    

    创建并实现测试的控制器

    package com.taotao.redisson.controller;
    
    
    import com.taotao.redisson.facade.GoodsStoreFacade;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.ResponseBody;
    @Controller
    @RequestMapping("/")
    public class TestController {
        
        @Autowired
        private GoodsStoreFacade goodsStoreService;
        
        /**
         * 进入测试页面
         * @param model
         * @return
         */
        @GetMapping("test")
        public ModelAndView stepOne(Model model){
            return new ModelAndView("test", "model", model);
        }
        
        /**
         * 秒杀提交
         * @param code
         * @param num
         * @return
         */
        @PostMapping("secKill")
        @ResponseBody
        public String secKill(@RequestParam(value="code",required=true) String code,@RequestParam(value="num",required=true) Integer num){
            String reString = goodsStoreService.updateGoodsStore(code, num);
            return reString;
        }
    }
    
    

    在static目录创建test.html,实现点击秒杀功能。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <script src="js/jquery-3.4.1.min.js"></script>
    
    
    </head>
    <body>
    <button id="btn_seckill">秒杀商品</button>
    <div id="count_num"></div>
    <div id="result"></div>
    
    <script type="text/javascript">
        var countNum = 0;
    
            $("#btn_seckill").click(function(){
    
                var json={"code":"2019053016502800101","num":1};
                for(var i = 0 ; i < 400 ; i++){
                      $.post("secKill",json,function(data){
                        if(data != "排队人数太多,请稍后再试."){
                            $("#result").append("<br />" + data + "<br />");
                        }else{
                            $("#result").append(data + " ");
          }
                        if(data.indexOf("恭喜您,购买成功") != -1){
                            countNum += 1;
                        }
                        $("#count_num").text("总共卖出:"+countNum);
                    });
                }
            });
    
    
    
    </script>
    </body>
    </html>
    
    
    

    相关文章

      网友评论

        本文标题:7,Spring Boot整合Redis实现简单的分布式锁

        本文链接:https://www.haomeiwen.com/subject/cdmxqhtx.html