SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:
一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下:
SET lock_key unique_value NX PX 10000
而解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end
这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。
org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.boot spring-boot-starter-jdbc org.projectlombok lombok true mysql mysql-connector-java com.alibaba druid-spring-boot-starter 1.1.22 com.baomidou mybatis-plus-boot-starter 3.3.1.tmp com.alibaba fastjson 1.2.7 io.springfox springfox-swagger2 2.7.0 com.github.xiaoymin swagger-bootstrap-ui 1.9.6 org.springframework.boot spring-boot-starter-test test
package com.example.springbootredisfbs.entity;import java.io.Serializable;
import lombok.Data;
import lombok.ToString;/*** (ShopOrder)实体类** @author qrxm* @since 2022-11-26 00:43:49*/
@Data
@ToString
public class ShopOrder implements Serializable {private static final long serialVersionUID = 229112699545570558L;/*** 订单id*/private String oid;/*** 用户id*/private Integer uid;/*** 用户名*/private String userName;/*** 商品id*/private Integer pid;/*** 商品名称*/private String pname;/*** 数量*/private Integer number;/*** 价格*/private Double price;/*** 订单编号*/private String orderSn;/*** 订单状态*/private Integer orderStatus;}
RedisConfig
package com.example.springbootredisfbs.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration //当前类为配置类
public class RedisConfig {@Bean //redisTemplate注入到Spring容器public RedisTemplate redisTemplate(RedisConnectionFactory factory){RedisTemplate redisTemplate=new RedisTemplate<>();RedisSerializer redisSerializer = new StringRedisSerializer();redisTemplate.setConnectionFactory(factory);//key序列化redisTemplate.setKeySerializer(redisSerializer);//value序列化redisTemplate.setValueSerializer(redisSerializer);//value hashmap序列化redisTemplate.setHashKeySerializer(redisSerializer);//key hashmap序列化redisTemplate.setHashValueSerializer(redisSerializer);return redisTemplate;}
}
Swagger2配置
package com.example.springbootredisfbs.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;/*** Swagger2配置*/
@Configuration
//@EnableWebMvc
@EnableSwagger2
public class Swagger2Config{@Beanpublic Docket createRestApi() {//规定扫描包下的注解return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).groupName("SpringBoot-Redis-分布式锁").select()//为当前包下的controller生成api文档.apis(RequestHandlerSelectors.basePackage("com.example.springbootredisfbs.controller")).paths(PathSelectors.any()).build();}private ApiInfo apiInfo() {//设置文档信息return new ApiInfoBuilder().title("测试接口文档").description("测试接口文档").contact(new Contact("浅若夏沫", "http:localhost:8080/doc.html","xxxx@xxxx.com")).version("1.0").build();}}
生成订单编号工具类
package com.example.springbootredisfbs.utils;import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicInteger;public class CodeGenerateUtils {private static final AtomicInteger SEQ = new AtomicInteger(1000);private static final DateTimeFormatter DF_FMT_PREFIX = DateTimeFormatter.ofPattern("yyMMddHHmmssSS");private static ZoneId ZONE_ID = ZoneId.of("Asia/Shanghai");/*** 订单号生成(NEW)* @return*/public static String generateOrderNo(){LocalDateTime dataTime = LocalDateTime.now(ZONE_ID);if(SEQ.intValue()>9990){SEQ.getAndSet(1000);}return dataTime.format(DF_FMT_PREFIX)+SEQ.getAndIncrement();}/*** 获取商品编码* 商品编码规则:nanoTime(后5位)*5位随机数(10000~99999)* @return*/public static String generateProductCode(){long nanoPart = System.nanoTime() % 100000L;if(nanoPart<10000L){nanoPart+=10000L;}long randomPart = (long)(Math.random()*(90000)+10000);String code = "0"+String.valueOf((new BigDecimal(nanoPart).multiply(new BigDecimal(randomPart))));return code.substring(code.length()-10);}
}
响应返回信息
package com.example.springbootredisfbs.utils;import com.example.springbootredisfbs.enums.ResultCodeEnum;
import lombok.Data;@Data
public class Result {private Integer code;private String message;private T data;// 构造器私有private Result(){}// 通用返回成功public static Result ok() {Result r = new Result<>();r.setCode(ResultCodeEnum.SUCCESS.getCode());r.setMessage(ResultCodeEnum.SUCCESS.getMessage());return r;}// 通用返回失败,未知错误public static Result error() {Result r = new Result<>();r.setCode(ResultCodeEnum.UNKNOWN_ERROR.getCode());r.setMessage(ResultCodeEnum.UNKNOWN_ERROR.getMessage());return r;}// 设置结果,形参为结果枚举public static Result setResult(ResultCodeEnum result) {Result r = new Result<>();r.setCode(result.getCode());r.setMessage(result.getMessage());return r;}/**------------使用链式编程,返回类本身-----------**/// 自定义返回数据public Result data(T map) {this.setData(map);return this;}// 自定义状态信息public Result message(String message) {this.setMessage(message);return this;}// 自定义状态码public Result code(Integer code) {this.setCode(code);return this;}
}
package com.example.springbootredisfbs.constant;public class OrderConstant {public static final String USER_ORDER_TOKEN_PREFIX = "order:token";
}
package com.example.springbootredisfbs.controller;import com.example.springbootredisfbs.entity.ShopOrder;
import com.example.springbootredisfbs.service.ShopOrderService;
import com.example.springbootredisfbs.utils.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;/*** (ShopOrder)表控制层** @author qrxm* @since 2022-11-25 23:56:22*/
@Slf4j
@RestController
@RequestMapping("shopOrder")
@Api(value = "测试接口", tags = "订单相关的接口")
public class ShopOrderController {/*** 服务对象*/@Resourceprivate ShopOrderService shopOrderService;@PostMapping("/addOrder")@ApiOperation(value = "创建订单信息")public Result addOrder(ShopOrder shopOrder) {log.info("【请求开始】创建订单信息,请求参数,body:{}", shopOrder);return shopOrderService.addOrder(shopOrder);}@PostMapping("/submit")@ApiOperation(value = "提交订单")public Result submitOrder(Integer uid, String orderSn, String token) {log.info("【请求开始】提交订单,请求参数,uid:{},orderSn:{},token:{}", uid, orderSn, token);return shopOrderService.submitOrder(uid, orderSn, token);}
}
订单状态
package com.example.springbootredisfbs.enums;public enum OrderStatusEnum {CREATE_NEW(0, "待付款"),PAYED(1, "已付款"),STAY_DELIVER(2, "待发货"),END_DELIVER(3, "已发货"),STAY_TAKE(4, "待收货"),RECIEVED(5, "已完成"),CANCLED(6, "已取消"),EVALUATE(7, "待评价"),SERVICING(8, "售后中"),SERVICED(9, "售后完成"),REFUNDING(10,"退款中"),REFUNDED(11,"已退款");private Integer code;private String msg;OrderStatusEnum(Integer code, String msg) {this.code = code;this.msg = msg;}public Integer getCode() {return code;}public String getMsg() {return msg;}
}
响应状态
package com.example.springbootredisfbs.enums;import lombok.Getter;@Getter
public enum ResultCodeEnum {SUCCESS(200,"操作成功"),ERROR(500,"操作失败"),UNKNOWN_ERROR(20001,"未知错误"),PARAM_ERROR(20002,"参数错误"),NULL_POINT(20003,"空指针异常"),HTTP_CLIENT_ERROR(20004,"接口请求异常");// 响应状态码private Integer code;// 响应信息private String message;ResultCodeEnum(Integer code, String message) {this.code = code;this.message = message;}
}
package com.example.springbootredisfbs.service;import com.example.springbootredisfbs.entity.ShopOrder;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.springbootredisfbs.utils.Result;/*** (ShopOrder)表服务接口** @author qrxm* @since 2022-11-25 23:56:22*/
public interface ShopOrderService extends IService {Result addOrder(ShopOrder shopOrder);Result submitOrder(Integer uid, String orderSn, String token);
}
package com.example.springbootredisfbs.service.impl;import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.api.R;
import com.example.springbootredisfbs.constant.OrderConstant;
import com.example.springbootredisfbs.entity.ShopOrder;
import com.example.springbootredisfbs.entity.ShopProduct;
import com.example.springbootredisfbs.enums.OrderStatusEnum;
import com.example.springbootredisfbs.service.ShopOrderService;
import com.example.springbootredisfbs.dao.ShopOrderDao;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.springbootredisfbs.utils.CodeGenerateUtils;
import com.example.springbootredisfbs.utils.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;/*** (ShopOrder)表服务实现类** @author qrxm* @since 2022-11-25 23:56:22*/
@Service("shopOrderService")
@Slf4j
public class ShopOrderServiceImpl extends ServiceImpl implements ShopOrderService {@Autowiredprivate StringRedisTemplate redisTemplate;@Overridepublic Result addOrder(ShopOrder shopOrder) {log.info("【请求开始】创建订单信息,请求参数,body:{}", shopOrder);if (shopOrder.getUid() == null) {log.error("用户没有登录");return Result.error().code(501).message("用户没有登录");}//TODO 1、生成订单信息//生成订单编号shopOrder.setOrderSn(CodeGenerateUtils.generateOrderNo());//订单状态 待支付shopOrder.setOrderStatus(OrderStatusEnum.CREATE_NEW.getCode());//TODO 2、购物车信息、订单详情//TODO 3、查看商品是否有库存ShopProduct product = new ShopProduct();product.setStock(5000);//判断库存数量要大于购买数量,否则库存不足if (product.getStock() < shopOrder.getNumber() ) {log.error("商品已没有库存了");return Result.error().code(502).message("商品已没有库存了");}//TODO 4、防重令牌(防止表单重复提交)//为用户设置一个token,三十分钟过期时间(存在redis)String token = UUID.randomUUID().toString().replace("-", "");redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + shopOrder.getUid(), token, 30, TimeUnit.MINUTES);Map map = new HashMap<>();map.put("token",token);map.put("order", shopOrder);return Result.ok().data(map);}@Overridepublic Result submitOrder(Integer uid, String orderSn, String token) {if (uid == null) {log.error("提交订单失败:用户未登录!");return Result.error().code(501).message("请登录");}if (orderSn == null) {return Result.error().code(401).message("参数不对");}//TODO 1、防重令牌(防止表单重复提交)//拿到令牌String orderToken = token;//解锁String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";// 原子验证和删除Long result = redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + uid), orderToken);if (result == 0) {// 验证令牌验证失败// 验证失败直接返回结果return Result.error().message("验证令牌验证失败").code(1);} else {// 原子验证令牌成功// 下单 创建订单、验证令牌、验证价格、验证库存// 1、创建订单、订单项信息// 2、验价// 3、保存订单// 4、库存锁定,只要有异常回滚订单数据//TODO 锁定库存,发送消息给MQ//TODO 订单创建成功,发送消息给MQreturn Result.ok();}}
}


解锁失败

解锁成功

上一篇:莫干山的竹林