情景:秒杀减库存操作,先从数据库查库存,然后再扣减
方案一:
用数据库的行锁,select * from xxx for update,将这行锁住,
缺点: 慢,很多数据会放在内存或redis中
方案二:
synchronized关键字 增加方法锁,
缺点:1慢,2而且无法做到细颗粒控制,比如有多个商品减库存会互相影响。3只适合单点应用,如果有集群不同用户就不能同步
@Service public class KillService { @Autowired RedisLock redisLock; static Map<String, Integer> products; static Map<String, Integer> stock; static Map<String, String> orders; private static final long TIMEOUT = 1000*10; static { products = new HashMap<>(); stock = new HashMap<>(); orders = new HashMap<>(); products.put("123456", 100000); stock.put("123456", 100000); } public String query(String prodId) { return "限量"+products.get(prodId)+"份,还剩:"+stock.get(prodId) +"份,下单数目:"+orders.size(); } public synchronized void kill(String prodId) { // 加锁 long time = System.currentTimeMillis()+TIMEOUT; if(!redisLock.lockNew(prodId, String.valueOf(Thread.currentThread()), 3)) { throw new SellException(101, "人太多,稍候再试"); } // 查库存 int stockNum = stock.get(prodId); if(stockNum==0) { throw new SellException(1000,"库存不足"); } // 下单 orders.put(KeyUtil.generateKey(), prodId); // 减库存 stockNum = stockNum -1; try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } stock.put(prodId, stockNum); // 解锁 redisLock.unlockNew(prodId, String.valueOf(Thread.currentThread())); } }
@Component public class RedisLock { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private StringRedisTemplate redisTemplate; long TIMEOUT = 1000*10; // 1.以客户端ID为value保证只有该客户端能解锁。2.设值和超时是同一个命令,原子性,防止中途崩溃,永远不超时 // public boolean lockNew(String key, String requestId, long timeout) { return redisTemplate.opsForValue().setIfAbsent(key, requestId, 10, TimeUnit.SECONDS); } // lua代码当成一个命令执行,eval()方法可以确保原子性 public boolean unlockNew(String key, String requestId) { String scriptText = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; DefaultRedisScript<Long> getRedisScript = new DefaultRedisScript<Long>(); getRedisScript.setResultType(Long.class); getRedisScript.setScriptText(scriptText); Long result = redisTemplate.opsForValue().getOperations().execute(getRedisScript, Collections.singletonList(key), requestId); logger.debug("----------redis:"+ result); // TODO return (result==1); }
// 以prodId为key, 超时时间为value, 通过判断value来是否超时获取锁 public boolean lock(String key, String value) { // 如果不存在则设置,存在则不处理 if(redisTemplate.opsForValue().setIfAbsent(key, value)) { return true; } // 两个线程同时到这里,他们的key和value一样,在下面设置时需要判断是否已经被另一个线程赋值了 String currentValue = redisTemplate.opsForValue().get(key); // 如果已经过期 if(!StringUtils.isEmpty(currentValue) && Long.valueOf(currentValue) < System.currentTimeMillis()) { // 设置并获取旧的值, 有问题!!!! 可能会覆盖别人的锁,get之后有人设了锁,不是原子性的 // 还有个问题是没有标识客户端无法,解锁时无法识别时不是本人 String oldValue = redisTemplate.opsForValue().getAndSet(key, value); // 没有被其他线程赋值,则返回成功 if(currentValue.equals(oldValue)) { return true; } } return false; } public boolean unlock(String key, String value) { try { String currentValue = redisTemplate.opsForValue().get(key); // 非原子性,中间若已经超时被别人加锁,会删掉别人的锁 if(value.equals(currentValue)) { return redisTemplate.opsForValue().getOperations().delete(key); } } catch (Exception e) { logger.error("【redis分布式解锁】异常,{}", e); } return false; } }