分布式锁应用最广的应该就是秒杀时库存的-1;此时我们可以采用redis的乐观锁和悲观锁实现:
需要考虑的问题:
1、用什么操作redis?幸亏redis已经提供了jedis客户端用于java应用程序,直接调用jedis API即可。
2、怎么实现加锁?“锁”其实是一个抽象的概念,将这个抽象概念变为具体的东西,就是一个存储在redis里的key-value对,key是于商品ID相关的字符串来唯一标识,value其实并不重要,因为只要这个唯一的key-value存在,就表示这个商品已经上锁。
3、如何释放锁?既然key-value对存在就表示上锁,那么释放锁就自然是在redis里删除key-value对。
4、阻塞还是非阻塞?笔者采用了阻塞式的实现,若线程发现已经上锁,会在特定时间内轮询锁。
5、如何处理异常情况?比如一个线程把一个商品上了锁,但是由于各种原因,没有完成操作(在上面的业务场景里就是没有将库存-1写入数据库),自然没有释放锁,这个情况笔者加入了锁超时机制,利用redis的expire命令为key设置超时时长,过了超时时间redis就会将这个key自动删除,即强制释放锁(可以认为超时释放锁是一个异步操作,由redis完成,应用程序只需要根据系统特点设置超时时间即可)。
1:乐观锁
redis中可以使用watch命令会监视给定的key,当exec时候如果监视的key从调用watch后发生过变化,则整个事务会失败。也可以调用watch多次监视多个key。这样就可以对指定的key加乐观锁了。注意watch的key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然了exec,discard,unwatch命令都会清除连接中的所有监视。
Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis最小的执行单位,一个事务中的命令要么都执行,要么都不执行。Redis事务的实现需要用到 MULTI 和 EXEC 两个命令,事务开始的时候先向Redis服务器发送 MULTI 命令,然后依次发送需要在本次事务中处理的命令,最后再发送 EXEC 命令表示事务命令结束。Redis的事务是下面4个命令来实现 :
1.multi,开启Redis的事务,置客户端为事务态。
// 2.exec,提交事务,执行从multi到此命令前的命令队列,置客户端为非事务态。
// 3.discard,取消事务,置客户端为非事务态。
// 4.watch,监视键值对,作用是如果事务提交exec时发现监视的键值对发生变化,事务将被取消。
@Override
public void run() {
try {
Thread.sleep((int)Math.random()*5000);//随机睡眠一下
} catch (InterruptedException e) {
e.printStackTrace();
}
while(true){
System.out.println("顾客:" + clientName + "开始抢购商品");
jedis = RedisUtil.getInstance().getJedis();
try {
jedis.watch(key);//监视商品键值对,作用时如果事务提交exec时发现监视的键值对发生变化,事务将被取消
int prdNum = Integer.parseInt(jedis.get(key));//当前商品个数
if (prdNum > 0) {
Transaction transaction = (Transaction) jedis.multi();//开启redis事务
((Jedis) transaction).set(key,String.valueOf(prdNum - 1));//商品数量减一
List<Object> result = ((redis.clients.jedis.Transaction) transaction).exec();//提交事务(乐观锁:提交事务的时候才会去检查key有没有被修改)
if (result == null || result.isEmpty()) {
System.out.println("很抱歉,顾客:" + clientName + "没有抢到商品");// 可能是watch-key被外部修改,或者是数据操作被驳回
}else {
jedis.sadd(clientList, clientName);//抢到商品的话记录一下
System.out.println("恭喜,顾客:" + clientName + "抢到商品");
break;
}
}else {
System.out.println("很抱歉,库存为0,顾客:" + clientName + "没有抢到商品");
break;
}
} catch (Exception e) {
// TODO: handle exception
}finally{
jedis.unwatch();
RedisUtil.returnResource(jedis);
}
}
}
2:悲观锁
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
while (true) {
if (Integer.parseInt(jedis.get(key)) <= 0) {
break;//缓存中没有商品,跳出循环,消费者线程执行完毕
}
//缓存中还有商品,取锁,商品数目减一
System.out.println("顾客:" + clientName + "开始抢商品");
if (redisBasedDistributedLock.tryLock(3,TimeUnit.SECONDS)) {//等待3秒获取锁,否则返回false(悲观锁:每次拿数据都上锁)
int prdNum = Integer.parseInt(jedis.get(key));//再次取得商品缓存数目
if (prdNum > 0) {
jedis.decr(key);//商品数减一
jedis.sadd(clientList, clientName);//将抢购到商品的顾客记录一下
System.out.println("恭喜,顾客:" + clientName + "抢到商品");
} else {
System.out.println("抱歉,库存为0,顾客:" + clientName + "没有抢到商品");
}
redisBasedDistributedLock.unlock0();//操作完成释放锁
break;
}
}
//释放资源
redisBasedDistributedLock = null;
RedisUtil.returnResource(jedis);
}
}