springboot:整合redis之分布式锁

x33g5p2x  于2022-06-27 转载在 Spring  
字(10.0k)|赞(0)|评价(0)|浏览(290)

springboot:整合redis之分布式锁

一、环境准备

依赖

<!-- RedisTemplate -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Redis-Jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

application.yaml配置文件

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 4000
    jedis:
      pool:
        max-wait: -1
        max-active: -1
        max-idle: 20
        min-idle: 10

二、配置类

public class ObjectMapperConfig {

    public static final ObjectMapper objectMapper;
    private static final String PATTERN = "yyyy-MM-dd HH:mm:ss";

    static {
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
        objectMapper = new ObjectMapper()
                // 转换为格式化的json(控制台打印时,自动格式化规范)
                //.enable(SerializationFeature.INDENT_OUTPUT)
                // Include.ALWAYS  是序列化对像所有属性(默认)
                // Include.NON_NULL 只有不为null的字段才被序列化,属性为NULL 不序列化
                // Include.NON_EMPTY 如果为null或者 空字符串和空集合都不会被序列化
                // Include.NON_DEFAULT 属性为默认值不序列化
                .setSerializationInclusion(JsonInclude.Include.NON_NULL)
                // 如果是空对象的时候,不抛异常
                .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
                // 反序列化的时候如果多了其他属性,不抛出异常
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                // 取消时间的转化格式,默认是时间戳,可以取消,同时需要设置要表现的时间格式
                .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
                .setDateFormat(new SimpleDateFormat(PATTERN))
                // 对LocalDateTime序列化跟反序列化
                .registerModule(javaTimeModule)

                .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
                // 此项必须配置,否则会报java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
                .enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY)
        ;
    }

    static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
        @Override
        public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            gen.writeString(value.format(DateTimeFormatter.ofPattern(PATTERN)));
        }
    }

    static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
        @Override
        public LocalDateTime deserialize(JsonParser p, DeserializationContext deserializationContext) throws IOException {
            return LocalDateTime.parse(p.getValueAsString(), DateTimeFormatter.ofPattern(PATTERN));
        }
    }

}
@Configuration
public class RedisConfig {

    /**
     * redisTemplate配置
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);

        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        jacksonSerializer.setObjectMapper(ObjectMapperConfig.objectMapper);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // 使用StringRedisSerializer来序列化和反序列化redis的key,value采用json序列化
        template.setKeySerializer(stringRedisSerializer);
        template.setValueSerializer(jacksonSerializer);

        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(stringRedisSerializer);
        template.setHashValueSerializer(jacksonSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

三、实体类和service

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Accessors(chain = true)
public class Product implements Serializable {

    private static final long serialVersionUID = 1L;

    private Integer id;

    /**
     * 商品名
     */
    private String name;

    /**
     * 库存
     */
    private Integer leftNum;
}
public interface ProductService {

    Product getById(Integer id);

    void updateById(Product product);

    List<Product> list();

}
@Service
public class ProductServiceImpl implements ProductService {

    private static final Map<Integer, Product> productMap;

    static {
        productMap = new HashMap<>();
        productMap.put(productMap.size() + 1, Product.builder().id(productMap.size() + 1).name("苹果").leftNum(10).build());
    }

    @Override
    public Product getById(Integer id) {
        return productMap.get(id);
    }

    @Override
    public void updateById(Product product) {
        Product update = productMap.get(product.getId());
        update.setLeftNum(product.getLeftNum());
        productMap.put(product.getId(), update);
    }

    @Override
    public List<Product> list() {
        return new ArrayList<>(productMap.values());
    }
}

四、方式一:lua脚本

@Slf4j
@Component
public class RedisLock {

   @Autowired
   private RedisTemplate<String, Object> redisTemplate;

   @Autowired
   private ProductService productService;

    private static final int TIMEOUT = 4000;
    private static final String LOCK_PREFIX = "secKill:";
    private final AtomicInteger i = new AtomicInteger(1);

    public void secKill(int productId) {
        // 查询库存
        Product product = productService.getById(1);
        if (product.getLeftNum() < 1) {
            log.error("库存不足");
            return;
        }

        // 加锁
        String uuid = UUID.randomUUID().toString();
        if (!lock(LOCK_PREFIX + productId, uuid)) {
            log.info("活动太火爆了,请稍后再操作");
            return;
        }

        //秒杀逻辑
        try {
            product.setLeftNum(product.getLeftNum() - 1);
            productService.updateById(product);
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 解锁
        unlock(LOCK_PREFIX + productId, uuid);
        log.info("秒杀成功" + i.getAndIncrement());
    }

    /**
     * 加锁
     */
    public boolean lock(String key, String value) {
        Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofMillis(TIMEOUT));
        if (ifAbsent != null && ifAbsent) {
            log.info("加锁成功");
            return true;
        }
        return false;
    }

    /**
     * 解锁(lua脚本原子性)
     */
    public void unlock(String key, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                "then return redis.call('del', KEYS[1]) " +
                "else return 0 " +
                "end";

        List<String> keys = new ArrayList<>();
        keys.add(key);
        Long execute = redisTemplate.execute(RedisScript.of(script, Long.class), keys, value);
        log.info("解锁成功 = " + execute);
    }
}
@RestController
public class ProductController {

    @Autowired
    private ProductService productService;
    @Autowired
    private RedisLock redisLock1;

    @GetMapping("/list")
    public List<Product> list() {
        return productService.list();
    }

    @GetMapping("/inr")
    public void inrLeft(Integer id) {
        productService.updateById(Product.builder().id(id).leftNum(10).build());
    }

    @GetMapping("/buy")
    public String buy(long time) {
        for (int i = 0; i < 100; i++) {
            try {
                new Thread(() -> redisLock1.secKill(1)).start();
                Thread.sleep(time);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return "秒杀结束";
    }
}

http://localhost:8080/list

http://localhost:8080/buy?time=50

http://localhost:8080/list

五、方式二:ThreadLocal

@Slf4j
@Component
public class RedisLock2 {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private ProductService productService;

    private static final int TIMEOUT = 4000;
    private static final String LOCK_PREFIX = "secKill:";
    private final AtomicInteger i = new AtomicInteger(1);
    private static final ThreadLocal<String> local = new ThreadLocal<>();

    public void secKill(int productId) {
        Product product = productService.getById(1);
        if (product.getLeftNum() < 1) {
            log.error("库存不足");
            return;
        }

        //加锁
        String uuid = UUID.randomUUID().toString();
        if (!lock(LOCK_PREFIX + productId, uuid)) {
            log.info("活动太火爆了,请稍后再操作");
            return;
        }

        //秒杀逻辑
        try {
            product.setLeftNum(product.getLeftNum() - 1);
            productService.updateById(product);
            Thread.sleep(80);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //解锁
        unlock(LOCK_PREFIX + productId, uuid);
        log.info("秒杀成功" + i.getAndIncrement());
    }

    /**
     * 加锁
     */
    public boolean lock(String key, String value) {
        Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofMillis(TIMEOUT));
        if (ifAbsent != null && ifAbsent) {
            log.info("加锁成功");
            local.set(value);
            return true;
        }
        return false;
    }

    /**
     * 解锁
     */
    public void unlock(String key, String value) {
        String localValue = local.get();
        if (localValue.equals(value) && localValue.equals(redisTemplate.opsForValue().get(key))) {
            log.info("解锁成功");
            redisTemplate.delete(key);
            local.remove();
        }
    }
}
@GetMapping("/buy2")
    public String buy2(long time) {
        for (int i = 0; i < 100; i++) {
            try {
                new Thread(() -> redisLock2.secKill(1)).start();
                Thread.sleep(time);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return "秒杀结束";
    }

六、方式三:AtomicInteger

@Slf4j
@Component
public class RedisLock3 {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private ProductService productService;

    private static final int TIMEOUT = 4000;
    private static final String LOCK_PREFIX = "secKill:";
    private final AtomicInteger ato = new AtomicInteger(1);

    public void secKill(int productId) {
        Product product = productService.getById(1);
        if (product.getLeftNum() < 1) {
            log.error("库存不足");
            return;
        }

        //加锁
        //value包含拥有者标识,具有唯一性,防止任何人都可以解锁
        String userId = UUID.randomUUID() + ":" + System.currentTimeMillis() + TIMEOUT;
        if (!lock(LOCK_PREFIX + productId, userId)) {
            log.error("活动太火爆了,请稍后再操作");
            return;
        }

        //秒杀逻辑
        try {
            product.setLeftNum(product.getLeftNum() - 1);
            productService.updateById(product);
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //解锁
        unlock(LOCK_PREFIX + productId, userId);
        log.info("秒杀成功" + ato.getAndIncrement());
    }

    /**
     * 加锁
     */
    public boolean lock(String key, String value) {
        //没有锁,持有并加锁
        Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(key, value);
        if (ifAbsent != null && ifAbsent) {
            log.info("加锁成功");
            return true;
        }

        //锁已被持有,判断锁过期
        synchronized (RedisLock3.class) {
            String redisValue = (String) redisTemplate.opsForValue().get(key);
            if (StringUtils.hasLength(redisValue) && Long.parseLong(redisValue.split(":")[1]) < System.currentTimeMillis()) {
                String oldValue = (String) redisTemplate.opsForValue().getAndSet(key, value);
                if (StringUtils.hasLength(oldValue) && oldValue.equals(redisValue)) {
                    log.info("锁过期,重新持有锁");
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 解锁
     */
    public void unlock(String key, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                "then return redis.call('del', KEYS[1]) " +
                "else return 0 " +
                "end";

        List<String> keys = new ArrayList<>();
        keys.add(key);
        Long execute = redisTemplate.execute(RedisScript.of(script, Long.class), keys, value);
        System.out.println("解锁成功 = " + execute);
    }

}
@GetMapping("/buy3")
    public String buy3(long time) {
        for (int i = 0; i < 100; i++) {
            try {
                new Thread(() -> redisLock3.secKill(1)).start();
                Thread.sleep(time);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return "秒杀结束";
    }

相关文章