Article Directory
- 1. Coupon flash sale
- 2. Distributed lock
-
- 2.1 Basic Principles
- 2.2 Comparison of Implementation Schemes
- 2.3 Redis Distributed Lock Implementation Ideas
- 2.4 Redis Distributed Lock Implementation - Primary Version
- 2.5 Redis Distributed Lock Implementation - Improvement
- 2.6 Lua script solves the atomicity problem of multiple commands
- 2.7 Summary
- 3. Redisson
1. Coupon flash sale
1.1 Overview of Globally Unique ID
Coupons can be issued for each store
When the user rushes to buy, the order will be generated and saved in the tb_voucher_order table, and there are some problems if the order table uses the database auto-increment ID
-
id regularity is too obvious
Users can guess some information based on the id.
For example, I placed an order for a product yesterday with an id of 10, and today I placed an order for a product with an id of 100, then I can guess that 90 products were sold during this period
-
Limited by the amount of data in a single table
As long as the user has an order behavior, new orders will continue to be generated.
When the website reaches a certain scale, there will be tens of millions of orders a year, and a single table will not be able to store such large-scale data after a few years.
We can only divide it into a single table for storage, and each table adopts the method of id auto-increment, which obviously cannot be realized, because each mysql table calculates its own self-increment, and it all starts from "1". Some data ids in the table may be repeated
If the id is repeated, there will be problems in querying the order in the future
To solve the above problem, we can use the global ID generator
The global ID generator is a tool used to generate a globally unique ID in a distributed system
Global meaning: Under the same business, no matter how many services, how many nodes, and how many different tables are divided into the distributed system in the future, as long as the id obtained by using the global ID generator must be unique in the current business id, there will be no conflict
Of course, it doesn’t matter if the ids of different businesses conflict
Monolithic projects also use the global ID generator
Generally, the following characteristics must be met
-
uniqueness
-
high availability
At least use it and you can't hang it up
-
high performance
Generate id correctly, and generate id faster
-
increment new
To have a monotonically increasing property. Although it is not self-increasing like a database, we need to ensure that the overall id is a process of gradually increasing, which is conducive to creating indexes for the database
-
safety
Regularity should not be too obvious
There is an incr command in our Redis command
First of all, uniqueness can be ensured, because Redis is independent of the database, no matter how many tables or databases the database has, there is only one Redis, so when everyone visits Redis, its self-increment must be unique
High availability, high performance, and incrementality are not explained
Security is a bit more troublesome, the key depends on what self-increment we use , if it is still 1, 2, 3, 4, ... then the security will definitely be poor
In order to ensure security, we can stitch some other information
Considering the performance of the database, our id still uses the numeric type (Long)
The first digit is always 0, which means our sign is always a positive number
Putting a 31-bit timestamp in the middle part actually increases the complexity of the id, why is it 31 bits, because we want to use seconds as the unit (we need to define an initial time as a benchmark, such as 2000, these 31 bits represent seconds , just calculate the time difference from 2000, 31 bits can represent up to 69 years)
The last part places the serial number, which is the auto-increment value of the Redis command incr
In this way of planning, it can support 2 to the 32 power orders in one second
Globally unique ID generation strategy
-
UUID:
Use JDK's built-in tool class to generate a long string of hexadecimal values. Because it is hexadecimal, it returns a string structure, and there is no monotonically increasing feature.
-
Redis self-increment
has been analyzed above
-
snowflake snowflake algorithm
It is also a 64-bit number of long type, and the self-increment adopts the self-increment of the current machine, which is internally maintained, so a mechanical id must be maintained
-
database self-increment
Redis self-increment ID strategy
- One key per day, convenient to count orders
- Limit the auto-increment value so that it does not exceed the upper limit
- ID construction is timestamp + counter
1.2 Redis implements globally unique ID
Implementation code
@Component
public class RedisIdWorker {
// 2022年1月1日0时0分0秒 的 秒数
private static final long BEGIN_TIMESTAMP = 1640995200L;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* id生成策略
*
* @param keyPrefix 业务前缀
* @return 生成的id
*/
public long nextId(String keyPrefix) {
// TODO 1.生成时间戳(当前时间减去我们规定的开始时间的秒数)
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// TODO 2.生成序列号
// 同一个业务不要使用同一个key,因为incr自增有限度,2的64次方
// 为了预防订单数日积月累超过2的64次方,我们可以再拼接一个日期字符串,这样做还能方便以后统计
// TODO 2.1 获取当前日期
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// TODO 2.2 自增长
// 返回值是Long,但是我们改成了long,改完出现警告:会有空指针问题
// 但是并不会出现空指针问题,加入此key不存在,它会在自动帮我们创建一个key
long count = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + ":" + date);
// TODO 拼接两部分
// 我们的返回值是long,直接拼接timestamp与count就是字符串了,不能直接拼接
// 为了解决这个问题,我们使用位运算
// timestamp<<32 时间戳向左移动32位,把redis自增的数创建出来,空出来的数以0位补充
// | 代表或运算,一个为真,就是真 0|0=0, 0|1=1,因为后面32位都是0,所以还是count本身
return timestamp<<32 | count ;
}
// public static void main(String[] args) {
// LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
// 传入一个时区作为参数
// long second = time.toEpochSecond(ZoneOffset.UTC);
// System.out.println(second);
// }
}
Test below
Test in the test class. If it keeps turning around, close the Redis client and reconnect it.
@Resource
private RedisIdWorker redisIdWorker;
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
void testIdWorker() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id = " + id);
}
latch.countDown();
};
long begin = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
es.submit(task);
}
System.out.println("laizhelile");
latch.await();
long end = System.currentTimeMillis();
System.out.println("time = " + (end - begin));
}
1.3 Add Coupon
Cheap and special coupons. Parity coupons can be purchased at will, while special coupons need to be snapped up
-
tb_voucher : Basic information of cheap coupons, coupon amount, usage rules
There is no stock in the following list, anyone can buy it at will
- tb_seckill_voucher : Special coupon inventory, start time of buying, end time of buying
1.3.1 entity
Voucher class, which also contains the key information of flash coupons, inventory, start time, end time
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_voucher")
public class Voucher implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 商铺id
*/
private Long shopId;
/**
* 代金券标题
*/
private String title;
/**
* 副标题
*/
private String subTitle;
/**
* 使用规则
*/
private String rules;
/**
* 支付金额
*/
private Long payValue;
/**
* 抵扣金额
*/
private Long actualValue;
/**
* 优惠券类型
*/
private Integer type;
/**
* 优惠券类型
*/
private Integer status;
/**
* 库存
*/
@TableField(exist = false)
private Integer stock;
/**
* 生效时间
*/
@TableField(exist = false)
private LocalDateTime beginTime;
/**
* 失效时间
*/
@TableField(exist = false)
private LocalDateTime endTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
SeckillVoucher class, seckill coupon class, the purpose is to supplement the Voucher class
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_seckill_voucher")
public class SeckillVoucher implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 关联的优惠券的id
*/
@TableId(value = "voucher_id", type = IdType.INPUT)
private Long voucherId;
/**
* 库存
*/
private Integer stock;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 生效时间
*/
private LocalDateTime beginTime;
/**
* 失效时间
*/
private LocalDateTime endTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
1.3.2 Controller
/**
* 新增秒杀券
* @param voucher 优惠券信息,包含秒杀信息
* @return 优惠券id
*/
@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {
voucherService.addSeckillVoucher(voucher);
return Result.ok(voucher.getId());
}
Notice! We use Voucher to receive the information of the entity class to be added, including seckill information
1.3.3 Service layer
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
// 保存优惠券
save(voucher);
// 保存秒杀信息
SeckillVoucher seckillVoucher = new SeckillVoucher();
seckillVoucher.setVoucherId(voucher.getId());
seckillVoucher.setStock(voucher.getStock());
seckillVoucher.setBeginTime(voucher.getBeginTime());
seckillVoucher.setEndTime(voucher.getEndTime());
seckillVoucherService.save(seckillVoucher);
}
1.3.4 Testing
Request address: http://localhost:8081/voucher/seckill,
Add request header: authorization, the specific value can be copied on the page
{
"shopId": 1,
"title": "100元代金券",
"subTitle": "周一至周五均可使用",
"rules": "全场通用\n无需预约\n可无限叠加\n不兑现、不找零\n仅限堂食",
"payValue": 8000,
"actualValue": 10000,
"type": 1,
"stock": 10,
"beginTime": "2022-01-25T10:09:17",
"endTime": "2025-01-26T12:09:04"
}
Look at the display effect of the page again
1.4 Place an order with a coupon
Involved table tb_voucher_order order table
Currently the most important thing for us is user id, voucher id
Case : Two points need to be judged when placing an order
- Whether the flash sale has started or ended, if it has not started or has ended, you cannot place an order
- Whether the inventory is sufficient, if not enough, the order cannot be placed
1.4.1 entity
Coupon order entity class
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_voucher_order")
public class VoucherOrder implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.INPUT)
private Long id;
/**
* 下单的用户id
*/
private Long userId;
/**
* 购买的代金券id
*/
private Long voucherId;
/**
* 支付方式 1:余额支付;2:支付宝;3:微信
*/
private Integer payType;
/**
* 订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款
*/
private Integer status;
/**
* 下单时间
*/
private LocalDateTime createTime;
/**
* 支付时间
*/
private LocalDateTime payTime;
/**
* 核销时间
*/
private LocalDateTime useTime;
/**
* 退款时间
*/
private LocalDateTime refundTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
1.4.2 Controller
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
@Resource
private IVoucherOrderService voucherOrderService;
@PostMapping("seckill/{id}")
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
return voucherOrderService.seckillVoucher(voucherId);
}
}
1.4.3 Service
The id of the coupon is the same as the id of the seckill coupon
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// TODO 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// TODO 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀活动尚未开始");
}
// TODO 3.判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀活动已经结束");
}
// TODO 4.判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
// TODO 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock-1")
.eq("voucher_id", voucherId)
.update();
if (!success) {
return Result.fail("扣减失败,可能库存不足");
}
// TODO 6.创建订单
// 我们只管订单id,代金券id,下单用户id
VoucherOrder voucherOrder = new VoucherOrder();
// TODO 6.1 订单id
// 使用自定义id生成器生成id
long orderID = redisIdWorker.nextId("order");
voucherOrder.setId(orderID);
// TODO 6.2 用户id
// 我们之前编写的登录拦截器
UserDTO user = UserHolder.getUser();
voucherOrder.setUserId(user.getId());
// TODO 6.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// TODO 7.返回订单id
return Result.ok(orderID);
}
You can take a look at this article, there is an explanation of the UserHolder login interceptor
SMS login based on Session_c# SMS verification code login_I love Brown Bear's Blog-CSDN Blog
1.4.3 Testing
Data appears in the order table
And the number of flash coupons is also reduced by 1
1.5 Inventory oversold problem
In the real seckill scenario, there are countless users rushing to buy together, click the button, and the number of concurrency per second may be hundreds or even thousands
We use Jmeter to test the program just now, create 200 threads to snap up coupons, but the number of coupons in the database is 100, it stands to reason that 100 people should not be able to grab them
But looking at the final result graph, I found that 45% of the people failed, which is supposed to be 50%.
Look at the amount of the database, the inventory is -9
In the high-concurrency scenario, the inventory oversold problem occurs. This is a problem that is easy to occur in the spike scenario, and it is also an unacceptable problem.
1.5.1 Reasons for oversold inventory
The high concurrency scenario remains the same, assuming we still have 1 item in stock
Thread 2 inquires again after thread 1 inquires, and finds that there is still inventory
This is only two threads, if there are more threads, it will be more troublesome, and the deduction will be more
The overselling problem is a typical multi-thread security problem. A common solution to this problem is to add locks
Pessimistic locks and optimistic locks are not real locks, but a concept of lock design
Pessimistic locking affects efficiency, and it is not easy to use under the conditions of multi-threaded concurrency.
Moreover, we only have multi-threaded concurrency problems in rare cases, and it is not very good to use pessimistic locks
Optimistic locking is quite optimistic, so we use optimistic locking
1.5.2 Introducing optimistic locking
The key to optimistic locking is to judge whether the data obtained by the previous query has been modified. There are two common ways:
-
version number issued
The most widely used and universal.
Add a version version to the field. Whenever the data is modified, the version number will be +1. If you want to determine whether the data has been modified, just compare the version number.
- CAS method
In fact, some optimizations have been made to the version number method. The version number is actually used to mark whether the data has been modified.
In fact, we can use inventory instead of version, and directly compare whether the inventory quantity has changed when updating
1.5.3 Optimistic lock to solve oversold inventory
We use the CAS method to modify the previous business logic
Only need to modify the fifth step and add a new condition
// TODO 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock-1") //set stock = stock-1
.eq("voucher_id", voucherId) //where voucher_id= voucherId
.eq("stock",voucher.getStock())//where stock= stock
.update();
if (!success) {
return Result.fail("扣减失败,可能库存不足");
}
After the modification, it was found that the abnormal rate was as high as 89%, and the number of people who failed greatly increased. It is better not to change it.
Observe the database and find that the inventory is not sold more
Why is it so wrong if it is not sold out?
It involves a disadvantage of optimistic locking, the success rate is too low, as shown in the figure below
Improvements to optimistic locking
It is very easy to change the inventory, as long as the inventory is greater than 0
// TODO 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock-1") //set stock = stock-1
.eq("voucher_id", voucherId) //where voucher_id= voucherId
.gt("stock",0)//where stock>0
.update();
if (!success) {
return Result.fail("扣减失败,可能库存不足");
}
Perfectly met our expectations
In addition, in order to improve the success rate, you can use the method of locking in batches, or the method of locking in segments
We can divide data resources into multiple shares. For example, the total inventory is 100 points, and it is stored in ten tables. The inventory in each table is 10. When the user grabs it, he can go to multiple tables to grab it, and the success rate is increased by 10 times.
1.6 Realize the function of one person, one order
Requirement : Modify the seckill business, require the same coupon, and a user can only place an order
It's just one more judgment than before, no big deal! ! ! ! !
The content of the TODO comment mark is newly added by us. We must first determine whether the order exists, and then decide whether it is enough to deduct the inventory
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀活动尚未开始");
}
// 3.判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀活动已经结束");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
// TODO 新增一人一单的判断
UserDTO user = UserHolder.getUser();
// TODO 查询订单
int count = query().eq("user_id", user.getId())
.eq("voucher_id", voucherId).count();
// TODO 判断是否存在
if (count > 0) {
// TODO 用户至少下过一单,不能再下了
return Result.fail("一人一单,不可重复下单");
}
// TODO 说明没买过,继续执行代码
// 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock-1") //set stock = stock-1
.eq("voucher_id", voucherId) //where voucher_id= voucherId
.gt("stock", 0)//where stock>0
.update();
if (!success) {
return Result.fail("扣减失败,可能库存不足");
}
// 6.创建订单
// 我们只管订单id,代金券id,下单用户id
VoucherOrder voucherOrder = new VoucherOrder();
// 6.1 订单id
// 使用自定义id生成器生成id
long orderID = redisIdWorker.nextId("order");
voucherOrder.setId(orderID);
// 6.2 用户id
// 我们之前编写的登录拦截器
voucherOrder.setUserId(user.getId());
// 6.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderID);
}
but! ! ! Under high concurrent threads (200 threads, same user), the exception rate is 95%, obviously there is a problem, and 10 threads successfully placed an order
Although a one-person-one-order judgment has been made in the code, the problem still exists
Same as oversold inventory, in a high-concurrency scenario, there may be some threads that execute the content marked in red below almost at the same time, that is to say, the verification of "one person, one order" has passed, and since it passes, the order can be generated As a result, the requirement of "one person, one order" has not been fulfilled
1.6.1 Pessimistic locking solves the "one person, one order" multi-thread concurrency problem
In which section to add pessimistic locking?
Verify one order per person, deduct inventory, and create an order.
Note where we lock! !
-
Cannot be added to the createVoucherOrder method
If synchronized is added to the method, it means that the synchronization lock is this, the current object
It is not recommended to put synchronized on the method. After locking this object, no matter which user comes, it is the lock, which means that the entire method is serialized. The so-called "one person, one
order" only needs to One user can lock it, if it is not the same user, no need to lock it -
Cannot be added in the createVoucherOrder method
After the loading method, the locked range is a bit small, because we have not submitted the transaction, the lock is released, and before submitting the transaction to modify the database data, it is very likely that other threads enter the createVoucherOrder method to realize the judgment of one person, one order, query The order found does not exist, and the reason for the non-existence is probably that the transaction has not been committed
In order to avoid this situation, the lock cannot be added to the createVoucherOrder method
-
The lock is added to the return value of the seckillVoucher method
Wait for the createVoucherOrder method to be executed before releasing the lock. It has been written to the database, which is perfect.
synchronized (user.getId().toString().intern()) { return createVoucherOrder(voucherId); }
For more information about locks, please check the following article JavaSE - Multithreading Details_javase Multithreading_I Love Brown Bear's Blog-CSDN Blog
Why do things fail in createVoucherOrder ?
When seckillVoucher calls the createVoucherOrder method, it actually returns this.createVoucherOrder(voucherId);, this refers to the current object, not its proxy object, and if things want to take effect, it is because Spring has made a dynamic proxy for the current class, taking To its proxy object to do transaction processing
But this current object has no transaction function (this is one of several possibilities for Spring transaction failure)
One of the solutions is to get the proxy object of the current object, as shown below
synchronized (user.getId().toString().intern()) {
// 获取当前对象的代理对象 强转
VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
Doing so requires importing the following dependencies:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
And you also need to start the class to expose this proxy object, if you don't expose it, you can't get it
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class HmDianPingApplication {
....
}
full code
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀活动尚未开始");
}
// 3.判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀活动已经结束");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
UserDTO user = UserHolder.getUser();
synchronized (user.getId().toString().intern()) {
// 获取当前对象的代理对象 强转
VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
// 如果在方法上添加synchronized,说明同步锁是this,当前对象
// 不建议 把synchronized放在方法上,锁住此对象后,不管任何一个用户来了,都是这把锁,也就意味着整个方法被串行化了
// 所谓“一人一单”,只需要对同一个用户加锁即可,如果不是同一个用户,无需加锁
@Transactional
public Result createVoucherOrder(Long voucherId) {
// TODO 新增一人一单的判断
UserDTO user = UserHolder.getUser();
// user.getId().toString()转换成字符串也无法保证线程安全,因为每次的String都不一样
// 我们可以加一个intern,是一个字符串对象规范表示,回去字符串常量池中找一找和此字符串的值一样的字符串地址并返回
// TODO 查询订单
int count = query().eq("user_id", user.getId())
.eq("voucher_id", voucherId).count();
// TODO 判断是否存在
if (count > 0) {
// TODO 用户至少下过一单,不能再下了
return Result.fail("一人一单,不可重复下单");
}
// TODO 说明没买过,继续执行代码
// 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock-1") //set stock = stock-1
.eq("voucher_id", voucherId) //where voucher_id= voucherId
.gt("stock", 0)//where stock>0
.update();
if (!success) {
return Result.fail("扣减失败,可能库存不足");
}
// 6.创建订单
// 我们只管订单id,代金券id,下单用户id
VoucherOrder voucherOrder = new VoucherOrder();
// 6.1 订单id
// 使用自定义id生成器生成id
long orderID = redisIdWorker.nextId("order");
voucherOrder.setId(orderID);
// 6.2 用户id
// 我们之前编写的登录拦截器
voucherOrder.setUserId(user.getId());
// 6.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderID);
}
Supplement: Why can the method modified by @Transactional only be public
- Access rights: Non-public methods (such as private methods and protected methods) are invisible to external callers, so they do not have the ability to be intercepted and woven into transaction aspects. The public method can be accessed and called by any caller, so it can be intercepted by the transaction aspect.
- Proxy mode: Spring framework realizes the function through AOP (aspect-oriented programming)
@Transactional
, usually using dynamic proxy technology. Dynamic proxies can only create proxy objects for public methods, but cannot create proxy objects for non-public methods.
1.7 Thread Concurrency Security Issues Under Cluster
The operation of "one person, one order" just now is only suitable for use in a stand-alone situation
If our project faces high concurrency, we will deploy a project to multiple machines to form a load balancing cluster. In cluster mode, pessimistic lock synchronization will be challenged and some problems will arise
Start two copies of the service, the ports are 8081 and 8082 (we used 8081 before)
1. Configure port 8082 - Dserver.port will overwrite the port in the configuration file
After the startup is complete, a small cluster is formed
2. Modify the nginx.conf file in the conf directory of nginx to configure reverse proxy and load balancing
The place marked in red is the modified place
3. Reload the nginx configuration file
4. Set a breakpoint at the pessimistic lock
5. Prepare two following requests and send them in succession
The tokens are all the same, indicating that a user is used
Discovery: Both requests can enter the synchronized pessimistic lock
Consequences: the count is 0, which means that one person has placed two orders, and the requirement of "one person, one order" has not been fulfilled
Now we are two JVMs
The principle of locking inside a JVM is to maintain a lock monitor object inside the JVM, and now the lock object uses the user id, and it is in the constant pool, and there is only one pool in a JVM, then when the id is the same In the case of , the description is the same lock, and the monitor of the lock is the same
but! When we deploy a cluster, a new deployment means a brand new tomcat, which means a brand new JVM. The constant pools between JVMs are independent
As shown below
2. Distributed lock
To solve the thread concurrency security problem under the cluster, distributed locks must be used
2.1 Basic Principles
After the analysis of 1.7, we hope that all JVMs use the same lock monitor , so this lock monitor must be a lock monitor outside the JVM that can be seen by multiple JVM processes. mutual exclusion between processes
As shown below
Distributed lock : A lock that is visible and mutually exclusive to multiple processes in a distributed system or cluster mode.
-
Multi-thread visible : multiple JVMs can see
This is actually very easy to do, such as using Redis, MySQL
-
Mutual exclusion : No matter how many times a person visits, only one can succeed, and everyone else must fail
-
High availability : no matter what technology, this is a common feature
To ensure that the action of acquiring the lock does not often cause problems
-
High performance : no matter what technology, this is a common feature
After adding the lock, it becomes serialized execution, and if the action of acquiring the lock is also very slow, then the performance can only be worse
-
Security :
When acquiring a lock, some abnormal situations should be considered, such as acquiring the lock and not releasing it, what to do if it hangs up, and whether there will be a deadlock
2.2 Comparison of Implementation Schemes
Core : Realize mutual exclusion between multiple threads
MySQL database or other databases have a transaction mechanism. When we write, we will automatically assign a mutual exclusion lock to us. In this way, multiple transactions are mutually exclusive, and only one person can execute them. Use this principle to achieve. We can go to MySQL to apply for a mutex before executing the business, and then execute the business. After the business is executed, we submit the transaction and release the lock. When our business is abnormal, we can automatically roll back
MySQL supports master-slave mode with high availability
Security is also possible, and our lock can be released in time if there is an exception. After disconnecting, the lock will be released automatically
The implementation of Redis is to use the mutually exclusive command setnx. When storing data to redis, the storage can only be successful when the data does not exist. It is also very simple to release the lock, just delete the key.
High availability, support cluster and master-slave
The performance is also very good, much higher than MySQL
Security should be carefully considered. If the key cannot be deleted after a downtime, a deadlock will occur. However, we can use the key expiration mechanism to automatically release it after expiration. However, how long is the expiration time will be discussed later
Zookeeper uses the uniqueness and order of nodes to achieve mutual exclusion
We can use order to achieve mutual exclusion. For example, many threads go to Zookeeper to create threads, so that the id of each node is monotonically increasing. We can artificially stipulate that the one with the smaller id can acquire the lock successfully (the smallest is only one)
Or you can also use uniqueness, everyone creates nodes, and everyone has the same node name, so that only one person succeeds
Releasing the lock is very simple, just delete the node
Good usability, supports clusters
The performance is not very good, because Zookeeper emphasizes strong consistency, which will consume a certain amount of time for data synchronization between the master and slave stock index, and the performance is a little worse than redis
The security is very good, the nodes are all temporary nodes, once a failure occurs, they will be released automatically after disconnection
2.3 Redis Distributed Lock Implementation Ideas
When implementing distributed locks, two basic methods need to be implemented, acquiring locks and releasing locks
-
acquire lock
-
Mutual exclusion: ensures that only one thread can acquire the lock
You can use the setNx mutual exclusion feature. If there is data with key=lock in the cache, it cannot be added
setNx lock thread1
During the discussion, it is recommended to add a timeout period to avoid deadlocks caused by service downtime. The time cannot be designed too short, otherwise the lock will be released before the business is executed.
expire lock 10
We recommend that the above two statements be combined to avoid downtime before executing expire, where the parameter followed by ex represents 10s
set lock thread1 EX 10 NX
-
-
release lock
-
Manual release: just delete the corresponding key
del lock
-
Timeout release: add a timeout when acquiring a lock
expire lock 10
-
When acquiring a lock, it either succeeds or fails, so what should we do after the failure?
The lock provided by JDK has two mechanisms
- Blocking acquisition, the acquisition of the lock fails and then blocks the wait until someone releases the lock
- Non-blocking try to acquire, try to acquire the lock, if the acquisition fails, it will end immediately and return a result instead of waiting
Here we use non-blocking, because blocking has a certain waste of CPU, and blocking is relatively troublesome to implement
2.4 Redis Distributed Lock Implementation - Primary Version
2.4.1 Define the ILock interface
public interface ILock {
/**
* 尝试获取锁,非阻塞方式
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功;false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
2.4.2 Define SimpleRedisLock implementation class
public class SimpleRedisLock implements ILock {
// 业务名称,为了获取锁
private String name;
private StringRedisTemplate stringRedisTemplate;
// 锁的前缀
private static final String KEY_PREFIX = "lock:";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
// TODO 获取线程标识
long threadId = Thread.currentThread().getId(); //线程id,同一个JVM线程id是不会重复的
// TODO 获取锁
// setIfAbsent是setnx
// 此处的value比较特殊,要加上线程的标识
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
// 直接返回success自动拆箱,会有安全风险。比如success为null,那拆箱后就是空指针
// 所以采取下面这种情况
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// TODO 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
2.4.3 Modify "One Person One Order Business"
You can delete the synchronized keyword in the seckillVoucher method in the VoucherOrderServiceImpl class, and we need to use a custom lock
The ones with the TODO logo are our new additions
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀活动尚未开始");
}
// 3.判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀活动已经结束");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
UserDTO user = UserHolder.getUser();
// TODO 创建锁对象
// 锁定的范围与之前一样。切记不能把order锁住,范围太大了,以后有关order的都被锁住了
SimpleRedisLock lock = new SimpleRedisLock("order:" + user.getId(), stringRedisTemplate);
// TODO 获取锁
// 订单大概是500ms,我们这里可以设定为5秒
boolean isLock = lock.tryLock(5);
// TODO 判断是否获取锁成功
if (!isLock) {
// TODO 不成功
// 我们要避免一个用户重复下单,既然获取锁失败,说明在并发执行,我们要避免并发执行
return Result.fail("不允许重复下单");
}
// TODO 成功
// createVoucherOrder方法执行过程中可能会有异常,我们放到try...catch中
try {
// 获取当前对象的代理对象 强转
VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}finally {
// TODO 出现异常做锁的释放
lock.unlock();
}
}
// 如果在方法上添加synchronized,说明同步锁是this,当前对象
// 不建议 把synchronized放在方法上,锁住此对象后,不管任何一个用户来了,都是这把锁,也就意味着整个方法被串行化了
// 所谓“一人一单”,只需要对同一个用户加锁即可,如果不是同一个用户,无需加锁
@Transactional
public Result createVoucherOrder(Long voucherId) {
// 新增一人一单的判断
UserDTO user = UserHolder.getUser();
// user.getId().toString()转换成字符串也无法保证线程安全,因为每次的String都不一样
// 我们可以加一个intern,是一个字符串对象规范表示,回去字符串常量池中找一找和此字符串的值一样的字符串地址并返回
// 查询订单
int count = query().eq("user_id", user.getId())
.eq("voucher_id", voucherId).count();
// 判断是否存在
if (count > 0) {
// 用户至少下过一单,不能再下了
return Result.fail("一人一单,不可重复下单");
}
// 说明没买过,继续执行代码
// 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock-1") //set stock = stock-1
.eq("voucher_id", voucherId) //where voucher_id= voucherId
.gt("stock", 0)//where stock>0
.update();
if (!success) {
return Result.fail("扣减失败,可能库存不足");
}
// 6.创建订单
// 我们只管订单id,代金券id,下单用户id
VoucherOrder voucherOrder = new VoucherOrder();
// 6.1 订单id
// 使用自定义id生成器生成id
long orderID = redisIdWorker.nextId("order");
voucherOrder.setId(orderID);
// 6.2 用户id
// 我们之前编写的登录拦截器
voucherOrder.setUserId(user.getId());
// 6.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderID);
}
carry out testing
One is true and the other is false, indicating that only port 8081 can acquire the lock successfully
2.4.4 Problem Analysis
The above code can run normally in most environments, but there are still some problems
Analyze the cause of the problem :
- Thread 1 is blocked due to business, and the lock expiration time is up, so it is automatically deleted
- When thread 1 is blocked, after the lock is deleted, thread 2 successfully acquires the lock to execute the business, but it has not finished executing
- At this time, thread 1 wakes up, executes the function of manually deleting the key, and deletes the lock of thread 2
- At this time, the new thread 3 finds that there is no lock to execute the business
In this way, thread 2 and thread 3 have thread safety issues
The most important reason is that thread 1 directly deletes other people's locks when releasing the lock.
Solution : When deleting the lock, check whether the lock belongs to its own thread
If it's not our lock, just leave it alone
2.5 Redis Distributed Lock Implementation - Improvement
two key points
- The first one: store the thread ID when acquiring the lock (can be identified by UUID)
We used the id before, the ready-made thread id
long threadId = Thread.currentThread().getId();
The id of a thread is actually an increasing number, and every time a thread is created inside the JVM, this number will increase
But if there are multiple JVMs in the cluster, each JVM will maintain an incremental number, which is likely to conflict
In order to resolve conflicts, we can use a UUID+thread id method, combine the two, use UUID to distinguish different JVMs, and then use thread id to distinguish different threads
- The second: when releasing the lock, first obtain the thread ID in the lock, and judge whether it is consistent with the current thread ID. If it is consistent, the lock will be released, if it is inconsistent, the lock will not be released
2.5.1 Modify the SimpleRedisLock implementation class
public class SimpleRedisLock implements ILock {
// 业务名称,为了获取锁
private String name;
private StringRedisTemplate stringRedisTemplate;
// 锁的前缀
private static final String KEY_PREFIX = "lock:";
//TODO UUID,使用胡图工具类, true表示将UUID生成的横线去掉
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
// TODO 获取线程标识
String threadId = ID_PREFIX+Thread.currentThread().getId(); //线程id,同一个JVM线程id是不会重复的
// 获取锁
// setIfAbsent是setnx
// 此处的value比较特殊,要加上线程的标识
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
// 直接返回success自动拆箱,会有安全风险。比如success为null,那拆箱后就是空指针
// 所以采取下面这种情况
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// TODO 获取线程标识
String threadId = ID_PREFIX+Thread.currentThread().getId();
// TODO 获取锁中标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// TODO 判断标识是否一致
if (threadId.equals(id)){
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
final rendering
2.5.2 Problem Analysis
It is not a perfect program so far, in some extreme cases, there are still some problems
Judging the lock and releasing the lock are two actions. Suppose there is a blockage when releasing the lock. If the blockage time is very long, the lock will be released by itself overtime, and other threads such as thread 2 can acquire the lock.
How could this be clogged? After the judgment, it is released immediately, and there is no code operation in the middle, so how can it be blocked?
There is a mechanism in the JVM called garbage collection, which will block all our code when doing this kind of operation. So it is not the code business blockage, but the blockage generated by the JVM itself
And just after thread 2 successfully acquires the lock, thread 1 is blocked, resumes running, and directly executes the action of releasing the lock (no more judgment, it is the blockage that occurred after the judgment), thus killing the lock of thread 2
At this time, thread 3 takes advantage of the gap and executes the business
Thread 2 and thread 3 have a thread safety problem
Solution: The action of judging the lock and releasing the lock becomes an atomic operation, which is executed together without gaps
2.6 Lua script solves the atomicity problem of multiple commands
Redis provides a Lua script function to write multiple Redis commands in one script to ensure the atomicity of multiple command execution.
A script can be understood as a function with a lot of code in it
Lua is a programming language, basic grammar reference: https://www.runoob.com/lua/lua-tutorial.html
Focus on the calling functions provided by Redis
# 执行Redis命令
redis.call('命令名称','key','其他参数',....)
For example, if we execute set name jack, the script is
redis.call('set','name','jack')
For example, to execute multiple commands, first execute set name Rose, and then execute get name, the script is as follows
# 先执行set name Jack
redis.call('get','name','jack')
# 再执行 get name
local name = redis.call('get','name')
# 返回
return name
After the script is written, how to call the script?
The following script is the script
For example, we want to execute the script redis.call('set','name','jack'), the syntax is as follows
#调用脚本, 脚本本身是一个字符串,用双引号引起来
# 0 代表着参数的数量,numkeys,因为脚本中是写死的,没有任何的参数
EVAL "return redis.call('set','name','jack')" 0
Scripts can pass parameters or not
How to write script language with parameters?
The parameters in Redis are divided into two categories, one is key type parameters (such as name), and the other is other types of parameters (Jack)
If the key and value in the script do not want to be hardcoded, they can be passed as parameters. Key type parameters will be placed in the KEYS array, and other parameters will be placed in the ARGV array. These parameters can be obtained from the KEYS and ARGV arrays in the script
The key and value of the following statement are not written to death.
In the lua language, the subscript of the array starts from 1
EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name Rose
2.6.1 Writing Lua scripts
Review the business process of releasing locks
- Get the thread ID in the lock
- Determine whether it is consistent with the specified ID (current thread ID)
- Release lock if consistent (delete)
- do nothing if inconsistent
Notice! The key cannot be written to death, and the keys of different businesses are different
-- 锁的key
--local key = "lock:order:5" 但是不能写死
local key = KEYS[1]
-- 当前线程标识
--local threadId = "UUID-10086" 但是不能写死
local key =ARGV[1]
-- 获取锁中的线程标识,也就是一个get命令
local id = redis.call('get',key)
-- 比较线程标识与锁中的标识是否一致
if(id == threadId) then
-- 释放锁 del key,删除成功return 1
return redis.call('del',key)
end
-- if不成立 return 0
return 0
2.6.2 Call lua script from Java - improve Redis distributed lock
Requirements : Realize the lock release logic of distributed locks based on Lua scripts
Tip : The API of RedisTemplate calling Lua script is as follows
Combined with EVAL for analysis :
The execute function is equal to EVAL, and RedisScript is equal to the following script, List<K>, which is a parameter of Key type, and Object args is actually an ARGV parameter
The code to release the lock is shown in the figure below. Part of the code is executed in the script, which can satisfy the atomicity
// TODO 提前获取锁的脚本
// RedisScript是一个接口,我们使用一个实现类,泛型就是返回值的类型
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
// TODO 静态代码块做初始化
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
// TODO 借助Spring提供的方法区resource下找
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
// TODO 配置一下返回值类型
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public void unlock() {
// TODO 调用脚本
// Key是一个集合,Collections.singletonList(KEY_PREFIX + name)可以快速生成一个集合
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId()
);
// 不要执行结果的返回值了,成功就是成功,不成功就是被删掉了
}
2.7 Summary
Redis-based distributed lock implementation ideas
- Use set nx ex to acquire the lock, and this is the expiration time, save the thread ID
- When releasing the lock, first judge whether the thread ID is consistent with itself, and delete the lock if it is consistent
characteristic
- Use set nx to satisfy mutual exclusion
- Use set ex to ensure that the lock can still be released when a fault occurs, avoiding deadlock and improving security
- Use Redis cluster to ensure high availability and high concurrency
3. Redisson
The distributed lock based on setnx has the following problems
-
not reentrant
Non-reentrant: the same thread cannot acquire the same lock multiple times
For example, a method a calls method b
In a, you need to acquire the lock first, execute the business and then call b, and in b you need to acquire the same lock. In this case, if the lock is not reentrant, the business logic cannot continue to be executed in b
In this case we require the lock to be reentrant
Reentrant: the same thread can acquire the same lock multiple times
-
no retry
Acquiring the lock only tries once and returns false, there is no retry mechanism
The lock we implemented before is non-blocking. If the lock acquisition fails, it will return false immediately. There is no retry mechanism, but it cannot be said to fail immediately in many businesses.
When we hope to find that the lock is occupied, I will wait and execute the business if it succeeds in the end
-
timeout release
Although the timeout release of the lock can avoid deadlock, if the business execution takes a long time, it will also cause the lock to be released, which poses a security risk (the problem of accidental deletion caused by the timeout release, there are also others)
-
master-slave consistency
If Redis provides a master-slave cluster, there is a delay in master-slave synchronization (it has not been synchronized to the slave Redis library). At this time, when the "master" goes down, we will choose a slave library as the new master library, but the new master library There is no lock mark for the data that has not been synchronized, which means that other threads can take advantage of the gap to get the lock
Security issues may occur in extreme cases, but the probability is relatively low
We can improve it with the help of frameworks
Redisson is a Java in-memory data grid (ln-Memory Data Grid) implemented on the basis of Redis. It not only provides a series of distributed common Java objects, but also provides many distributed services, including the implementation of various distributed locks.
A collection of distributed tools implemented on the basis of redis. He has all kinds of tools used in distributed systems, including distributed locks
官网:Redisson: Easy Redis Java client with features of In-Memory Data Grid
3.1 Redisson quick start
You can import the coordinates, then configure the Redis client, or configure it in the yaml file, and provide a Redisson-starter
The second method is not recommended, because it will replace Spring’s official configuration and implementation of Redisson, so it is recommended to use the first method and configure a Redisson by yourself
Maven coordinates
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
Configure the Redisson client
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient(){
// TODO 配置类
Config config = new Config();
// TODO 添加Redis地址,如单点地址(非集群)
// 我没有密码,如果有密码的话可以设.setPassword("")
// 如果是集群的话使用config.useClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// TODO 创建客户端
return Redisson.create(config);
}
}
Use Redisson distributed lock to transform the previous business
lock.tryLock(1,10,TimeUnit.SECONDS)
Parameter 1: The waiting time for acquiring the lock, that is to say, the action of tryLock is blocking. If the lock acquisition fails, it will wait for a period of time before retrying. If the lock is not acquired after retrying, it will return false (retry mechanism )
Parameter 2: Lock automatic release time, if the lock is not released after this time, it will be released automatically
Parameter 3: time unit
lock.tryLock(), if there is no parameter, it means not to wait, and returns directly if it fails
Code knowledge only changed the client, others do not need to be moved
@Resource
private RedissonClient redissonClient;
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀活动尚未开始");
}
// 3.判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀活动已经结束");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
UserDTO user = UserHolder.getUser();
// 创建锁对象
// 锁定的范围与之前一样。切记不能把order锁住,范围太大了,以后有关order的都被锁住了
// 之前的方式:SimpleRedisLock lock = new SimpleRedisLock("order:" + user.getId(), stringRedisTemplate);
// TODO 使用Redisson客户端获取锁对象
RLock lock = redissonClient.getLock("lock:order:" + user.getId());
// 获取锁
// 订单大概是500ms,我们这里可以设定为秒
boolean isLock = lock.tryLock();
// 判断是否获取锁成功
if (!isLock) {
// 不成功
// 我们要避免一个用户重复下单,既然获取锁失败,说明在并发执行,我们要避免并发执行
return Result.fail("不允许重复下单");
}
// 成功
// createVoucherOrder方法执行过程中可能会有异常,我们放到try...catch中
try {
// 获取当前对象的代理对象 强转
VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}finally {
// 出现异常做锁的释放
lock.unlock();
}
}
3.2 Principle of reentrant lock
Redisson can achieve reentrancy
Non-reentrant : the same thread cannot acquire the same lock multiple times
Reentrant : the same thread can acquire the same lock multiple times
For example, a method a calls method b
In a, you need to acquire the lock first, execute the business and then call b, and in b you need to acquire the same lock. In this case, if the lock is not reentrant, the business logic cannot continue to be executed in b
In this case we require the lock to be reentrant
Our previous custom locks cannot be reentrant , as shown in the following figure:
method1 first acquires the lock, and succeeds, then calls method2, method2 also acquires the lock, and then fails to acquire, because the corresponding key in redis already has a value, setnx will no longer succeed
If we want to implement a reentrant lock, we need to modify the previous String structure to a Hash structure
Not only to record the thread ID, but also to record how many times the lock is acquired, value+1 when the lock is acquired, and value-1 when the lock is released
When can we actually release the lock and delete the lock?
When the lock is released, the corresponding value of value is 0, and then it can be deleted
After we changed the String structure to the Hash structure, there is a big difference between acquiring and releasing locks
The setnx ex command is used for the String type. The setnx command is used to determine whether there is mutual exclusion and whether the lock can be successfully acquired, and the ex command sets the expiration time
However, after using Hash now, there is no such combination command. We can only judge whether the lock exists, and then manually set the expiration time. The flow chart is as follows
flow chart
For acquiring locks and releasing locks, we still need to use Lua scripts to achieve atomicity
Lua script to acquire lock
Lua script to release lock
3.3 Redisson lock retry and WatchDog mechanism
Let's discuss how Redisson solves non-retry and timeout release
lock.tryLock(1,10,TimeUnit.SECONDS)
Parameter 1: The waiting time for acquiring the lock, that is to say, the action of tryLock is blocking. If the lock acquisition fails, it will wait for a period of time before retrying. If the lock is not acquired after retrying, it will return false (retry mechanism )
Parameter 2: Lock automatic release time, if the lock is not released after this time, it will be released automatically
Parameter 3: time unit
lock.tryLock(), if there is no parameter, it means not to wait, and returns directly if it fails
**lock.tryLock(1L, TimeUnit.SECONDS);** If there are two parameters, the first parameter indicates the waiting time, and the second is the time unit. This code indicates that if the lock is not obtained within 1s, it will not waited
3.1.1 Lock Retry
We can look at the **lock.tryLock(1L, TimeUnit.SECONDS);** method
In the tryLock method, a full-parameter tryLock method is called, click in and take a look
In this method, the tryAcquire method is called again
Go ahead and look at the tryAcquireAsync method
Because we have not set the expiration time, there will be a default expiration time, as shown in the figure below
The value of the getLockWatchdogTimeout() method is 30000L, and the unit is milliseconds, which is 30s
Going down to execute, it is the method shown in the figure below
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
The command return redis.call('pttl', KEYS[1]); is to get the validity period or how long, ttl returns in seconds, and pttl returns in milliseconds
What is the use of getting the remaining validity period of the lock?
回到方法private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId
Get a Future object here
After return, continue to look backwards. When you get to the picture below, you get the remaining validity period, and then return
back to the original place
If ttl is null, it means that no other thread acquires the lock, we can acquire this lock and return true
If it is not null, it means that the acquisition of the lock failed, and if it fails, you need to try
time indicates how much of the set expiration time is left, if there is, try it, if not, return false directly
If time>0, the expiration time has not yet been reached, we wait to try, but this attempt is not an immediate attempt, but executes a subscribe method, a subscription method
What did you subscribe to? The signal that someone else who subscribed released the lock
In the Lua script, a message was published before, and we are now subscribing to this one
Then the following code is very clever, the time we receive the notification is uncertain, so await it, but how long is the await? It is time, the remaining lock release time
If the waiting time of await exceeds time, we will unsubscribe to cancel the subscription, because it has no meaning
If the await waiting time has not timed out, continue to go down, and then calculate the time to see if it is less than zero, if it is less than 0, it has timed out, and returns false (the source code time calculation is very rigorous, and it has been counted there)
If there is enough time, enter the while statement (as long as the time is left, it will keep looping), and execute the tryAcquire method, the same as before
3.1.2 Lock timeout
I don't quite understand, -20. Distributed locks - Redisson's lock retry and WatchDog mechanism
We should make sure that the lock is released by the business execution rather than the lock timeout after the business is blocked.
Go back to tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) method
ttlRemaining==null, indicating that we have successfully acquired the lock. At this time, we have to solve the problem of validity period
How to solve the problem of expiration date? Call the scheduleExpirationRenewal method
Among them, the renewExpiration method updates the validity period, let's take a look
Timeout is a timeout task or timing task, which executes the renewExpirationAsync method
Let's take a look at the renewExpirationAsync method
Execute the following script
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end;
return 0;
3.1.3 Summary
-
Reentrant : use Hash structure to record thread id and reentry times
The Hash structure replaces the String structure used by the original customization
One field of the Hash structure records the thread ID; the other field records the number of re-recordings
Every time you acquire a lock, first determine whether the lock exists. If it does not exist, acquire it directly. If it already exists, it will not necessarily fail at this time. If you find that the thread ID is the current thread, it means you can acquire it again, and add 1 to the number of reentries. That's it.
When releasing the lock, the number of releases is reduced by 1 until the number of reentries is reduced to 0, which proves that the outermost layer has been reached and all business is over, and the lock is actually released
-
Retryable : Use the semaphore and PubSub functions to implement the retry mechanism for waiting, waking up, and failing to acquire locks
After the first failure to acquire the lock, it does not fail immediately, but to wait for a message to release the lock. After getting the message, you can re-acquire the lock. If the acquisition fails again, continue to wait
Of course, it is not unlimited retries, there will be a waiting time, beyond which there will be no retries
-
Timeout renewal : use watchDog to reset the timeout period at regular intervals (releaseTime/3)
After the lock is successfully acquired, a timing task will be started. This task will reset the timeout period of the lock every once in a while, and reconfigure expire, so that the lock attempt time will be retimed in the future.
3.4 multiLock principle
3.4.1 Analysis
Let's take a look at the problem of "master-slave consistency"
**Let’s first take a look at what is master-slave? **In fact, it is multiple Redis, but their roles are different
We can use one of them as the master node Redis Master, and the other as the slave node Redis Slave.
In addition, the functions of the master and slave are also different, and the separation of reading and writing is often done. The main library performs addition, deletion, and modification operations, and some operations are read from the library
Since the master node does write operations and the slave nodes do read operations, then the slave nodes should have no data. Therefore, data synchronization between the master and the slave is required. The master node will continuously synchronize its data to the slave node to ensure that the data between the master and the slave is consistent, but there will be a certain delay between the master and the slave, so the master node There will also be a certain delay in the synchronization of the slave, and the consistency of the master-slave synchronization is precisely because of such a delay.
Suppose there is a problem with the main library and it goes down, there will be a sentinel in Redis to monitor the cluster status, and if it finds that the host is down, it will disconnect from it, and then select a library from other slave nodes as the new main library.
Because the master-slave synchronization has not been completed before, the lock has not been synchronized to the new main library. At this time, when the java application visits the new master node, it will find that the lock is gone, and the lock can be acquired, and there is a concurrent security problem.
How does Redis solve this problem?
Simple and rude, no more masters and slaves, all Redis masters and slaves become a single independent Redis node, there is no relationship between them, and they can all read and write.
At this point, the way we acquire locks has changed. Before, we acquired locks from the Master, but now we need to acquire locks from each Redis node in turn, and each Redis library has a lock logo.
When is this situation called successful lock acquisition? Each Redis Master acquires the lock successfully, which is called success
Then we can establish master-slave synchronization for these master nodes without worrying about thread safety
As shown below, after the top Node hangs up, the following slave node becomes the new master node. Obviously, there will be no locks, but the other two Node nodes have locks. In this way, two have locks and one has no locks. fetch failed
That is to say, if a node has a lock, other threads cannot get the lock
This scheme is called multiLock in Redisson
3.4.2 Testing
Reconfigure Redisson , port 6379,6380,6381
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// TODO 配置类
Config config = new Config();
// TODO 添加Redis地址,如单点地址(非集群)
// 我没有密码,如果有密码的话可以设,如果是集群的话使用config.useClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// TODO 创建客户端
return Redisson.create(config);
}
@Bean
public RedissonClient redissonClient2(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6380");
return Redisson.create(config);
}
@Bean
public RedissonClient redissonClient3(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6381");
return Redisson.create(config);
}
}
test code
@Slf4j
@SpringBootTest
class RedissonTest {
// TODO 三个注入客户端
@Resource
private RedissonClient redissonClient;
@Resource
private RedissonClient redissonClient2;
@Resource
private RedissonClient redissonClient3;
private RLock lock;
@BeforeEach
void setUp() {
// TODO
RLock lock1 = redissonClient.getLock("order");
RLock lock2 = redissonClient2.getLock("order");
RLock lock3 = redissonClient3.getLock("order");
// TODO 创建联锁,用哪个客户端调用没有区别
lock = redissonClient.getMultiLock(lock1,lock2,lock3);
}
// TODO 使用方式和之前没有区别,代码不用动
@Test
void method1() throws InterruptedException {
// 尝试获取锁
boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
if (!isLock) {
log.error("获取锁失败 .... 1");
return;
}
try {
log.info("获取锁成功 .... 1");
method2();
log.info("开始执行业务 ... 1");
} finally {
log.warn("准备释放锁 .... 1");
lock.unlock();
}
}
void method2() {
// 尝试获取锁
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("获取锁失败 .... 2");
return;
}
try {
log.info("获取锁成功 .... 2");
log.info("开始执行业务 ... 2");
} finally {
log.warn("准备释放锁 .... 2");
lock.unlock();
}
}
}
3.5 Summary
-
Non-reentrant Redis distributed lock
- Principle: use the mutual exclusion of setnx; use ex to avoid deadlock; release the thread mark when the lock is released
- Defects: non-reentrant, unable to retry, lock timeout failure
-
Reentrant Redis distributed lock
- Principle: Use the hash structure to record thread marking and reentry times; use watchDog to extend the lock time; use semaphores to control lock retry waiting
- Defect: lock failure caused by redis downtime
-
Redisson的multiLock
- Principle: Using multiple independent Redis nodes, reentrant locks must be obtained on all nodes in order to obtain locks successfully