怎么保证原子性操作呢?
1 数据库:
update product set left_num=left_num-1 where left_num>0;
这里用到的是left_num=left_num-1,如果left_num>0才能执行成功,数据库查询、更新的时候有用到锁,是可以保证更新操作的原子性的。
数据库性能较差,不建议使用。
2 分布式锁
分布式锁一般可以用以下方式实现:
- 数据库乐观锁;
- 基于Redis的分布式锁;
- 基于ZooKeeper的分布式锁。
如果用redis来做一个分布式锁,大家都在等待锁解开,不建议使用。
首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:
引入依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
实现分布式锁的代码:
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
解锁代码
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
可以看到,我们解锁只需要两行代码就搞定了! 第一行代码,我们写了一个简单的Lua脚本代码。
第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。
eval()方法是将Lua代码交给Redis服务端执行。那么这段Lua代码的功能是什么呢?
其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
3 消息队列
将订单请求全部放入消息队列,然后另外一个后台程序一个个处理队列中的订单请求。
并发不受影响,但是用户等待的时间较长,进入队列的订单也会很多,体验上并不好,也不建议使用。
4 redis递减/递增
通过 redis->incrby(‘product’, -1) 得到递减之后的库存数。
本系统是递增直到到达售卖数量,即库存量就不再处理请求。
性能方面很好,同时体验上也很好
ps(好是挺好,就是qps不高,只有500左右,对于并发量没有太高要求的项目可以使用这种方法)
5 第三方工具redission或lettuce(最优解)
现在我们就来说说具体的redission的配置,通过配置之后就可以使用了,没用什么特别复杂的地方,先上demo,在根据demo来说说细节。
import java.io.IOException;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfig {
@Bean(destroyMethod = "shutdown")
RedissonClient redisson() throws IOException {
Config config = new Config();
//config.useClusterServers().addNodeAddress("127.0.0.1:6379");
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
redission的配置比较简单,配置一个RedissonClient就可以了,默认的配置支持redis的所有实现方式,如集群,单节点,哨兵,副本等都可以进行配置,这里需要注意的是配置的线程模型,默认配置的线程是64个,我们根据需要来进行配置及连接超时时间,请求超时时间,是否长连接等参数进行配置,这里没有什么特别的参数,基本就是服务器连接的一些参数配置,根据实际业务进行性能优化的点大部分都在这里进行控制,代码的性能优化主要还是根据规范对key的可读性,长度等进行控制,对值得长度进行控制,对结合的元素个数进行控制,以及序列化的性能,主要就是这几方面的处理。
import java.util.concurrent.TimeUnit;
import org.redisson.api.RBucket;
import org.redisson.api.RCountDownLatch;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class RedisDao {
@Autowired
private RedissonClient redissonClient;
// -----------------------------------------------------------------------
public String getString(String key) {
RBucket<Object> result = this.redissonClient.getBucket(key);
return result.get().toString();
}
public void setString(String key, Object value) {
RBucket<Object> result = this.redissonClient.getBucket(key);
if (!result.isExists()) {
result.set(value, 5, TimeUnit.MINUTES);
}
}
public boolean hasString(String key) {
RBucket<Object> result = this.redissonClient.getBucket(key);
if (result.isExists()) {
return true;
} else {
return false;
}
}
public long incr(String key, long delta) {
return this.redissonClient.getAtomicLong(key).addAndGet(delta);
}
// -----------------------------------------------------------------------
public void lock() {
RCountDownLatch countDown = redissonClient.getCountDownLatch("aa");
countDown.trySetCount(1);
try {
countDown.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
RCountDownLatch latch = redissonClient.getCountDownLatch("countDownLatchName");
latch.countDown();
RReadWriteLock rwlock = redissonClient.getReadWriteLock("lockName");
rwlock.readLock().lock();
rwlock.writeLock().lock();
rwlock.readLock().lock(10, TimeUnit.SECONDS);
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
try {
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
boolean res1 = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
redission中包含了我们了解的常用锁的类型,基本的可重入锁,读写锁,以及CountDownLatch的设置及使用,但是他们是分布式锁,以往我们JUC提供的锁都是在单线程的线程模型中使用的,当多个进程多个线程来操作一个无锁的共享资源的时候,就会出现线程不安全的问题,就是我们多次执行后结果和单个线程执行时结果的不一致,为了让线程一致我们是需要一些处理办法的,那就是分布式锁,通过锁进行多线程的同步来进行资源隔离来实现对资源的访问控制,从而达到线程安全,所以根据实际中的业务需要,我们可以根据自身的技术实力及业务需要来实现自己的分布式锁,实现的方式主要有redis和zookeeper,有需要的可以自行百度学习,稍后博主自己也会对该方面知识进行学习整理。通过上面的整理相信大家已经对redission已经有了一个初步的认识,具体的编程语法需要自己去根据数据类型具体学习。